Go 博客

Go 1.7 编译出的二进制文件更小

David Crawshaw
2016 年 8 月 18 日

简介

Go 被设计用于编写服务器。如今,它被最广泛地用于此目的,因此,运行时和编译器上的大量工作都集中在对服务器至关重要的问题上:延迟、易于部署、精确的垃圾回收、快速的启动时间、性能。

随着 Go 被用于越来越多的程序,必须考虑新的问题。其中之一是二进制文件的大小。这个问题已经存在很长时间了(问题 #6853 在两年前就被提交了),但人们越来越关注在更小的设备上部署 Go 二进制文件(例如 Raspberry Pi 或移动设备),这意味着 Go 1.7 版本对此给予了一些关注。

Go 1.7 中完成的工作

Go 1.7 中的三个重大更改影响了二进制文件的大小。

第一个是此版本中为 AMD64 启用的新的 SSA 后端。虽然 SSA 的主要动机是提高性能,但生成的代码更好,也更小。SSA 后端使 Go 二进制文件缩小了约 5%。我们预计,当 Go 1.8 中 ARM 和 MIPS 等更类似 RISC 的架构的后端转换为 SSA 时,将获得更大的收益。

第二个更改是方法修剪。在 1.6 之前,所有已用类型上的所有方法都将保留,即使某些方法从未被调用过。这是因为它们可能会通过接口被调用,或者使用 reflect 包动态调用。现在,编译器会丢弃任何与接口不匹配的未导出方法。类似地,链接器可以丢弃其他导出方法,即只能通过反射访问的方法,如果程序中的任何位置都没有使用相应的 反射功能。此更改使二进制文件缩小了 5% 到 20%。

第三个更改是 reflect 包使用的运行时类型信息的更紧凑格式。编码格式最初旨在使运行时和 reflect 包中的解码器尽可能简单。通过使此代码更难阅读,我们可以在不影响 Go 程序运行时性能的情况下压缩格式。新格式使 Go 二进制文件进一步缩小了 5% 到 15%。为 Android 构建的库和为 iOS 构建的归档文件进一步缩小,因为新格式包含更少的指针,每个指针都需要在位置无关代码中进行动态重定位。

此外,还有许多小的改进,例如改进的接口数据布局、更好的静态数据布局以及简化的依赖项。例如,HTTP 客户端不再链接整个 HTTP 服务器。可以在问题 #6853 中找到所有更改的完整列表。

结果

使用 Go 1.7 构建的典型程序(从小型玩具到大型生产程序)大约缩小了 30%。

规范的 hello world 程序从 2.3MB 缩小到 1.6MB

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

在不包含调试信息的情况下编译时,静态链接的二进制文件现在小于 1MB。

一个用于测试此周期的大型生产程序 jujud 从 94MB 缩小到 67MB。

位置无关二进制文件缩小了 50%。

在位置无关可执行文件 (PIE) 中,只读数据段中的指针需要动态重定位。由于类型信息的新格式用段偏移量替换了指针,因此每个指针可以节省 28 字节。

删除了调试信息的位置无关可执行文件对移动开发人员尤其重要,因为这是发送到手机的程序类型。大型下载会带来糟糕的用户体验,因此这里的减少是个好消息。

未来的工作

对运行时类型信息的一些更改来得太晚,无法在 Go 1.7 冻结之前完成,但希望它们能进入 1.8,进一步缩小程序,尤其是位置无关程序。

这些更改都是保守的,在不增加构建时间、启动时间、整体执行时间或内存使用量的情况下减小了二进制文件的大小。我们可以采取更激进的措施来减小二进制文件的大小:upx 工具用于压缩可执行文件,可以使二进制文件进一步缩小 50%,但代价是增加了启动时间并可能增加了内存使用量。对于极小的系统(可能存放在钥匙扣上的那种),我们可以构建一个没有反射的 Go 版本,尽管不清楚这种受限的语言是否足够有用。对于运行时中的一些算法,当每千字节都很重要时,我们可以使用速度较慢但更紧凑的实现。所有这些都需要在以后的开发周期中进行更多研究。

感谢许多帮助使 Go 1.7 二进制文件更小的贡献者!

下一篇文章:使用子测试和子基准测试
上一篇文章:Go 1.7 发布
博客索引