Go 博客
Go 1.7 二进制文件更小
引言
Go 的设计初衷是为了编写服务器。如今它最广泛的用途也是如此,因此运行时和编译器的大量工作都集中在对服务器至关重要的方面:延迟、易于部署、精确的垃圾回收、快速启动时间和性能。
随着 Go 被用于更多种类的程序,需要考虑新的问题。其中之一就是二进制文件的大小。这个问题已经存在了很长时间(两年多前就提交了 issue #6853),但随着在部署到较小设备(如树莓派或移动设备)上使用 Go 部署二进制文件的兴趣日益浓厚,Go 1.7 版本对此给予了一些关注。
Go 1.7 中的工作
Go 1.7 中影响二进制文件大小的三个重要变化。
第一个是本次发布中为 AMD64 启用的新 SSA 后端。虽然 SSA 的主要动机是提高性能,但生成的代码也更小。SSA 后端将 Go 二进制文件缩小了约 5%。我们预计当 ARM 和 MIPS 等更精简的 RISC 架构在 Go 1.8 中转换为 SSA 后端时,会有更大的提升。
第二个变化是方法剪枝。直到 1.6 版本,所有已使用类型的全部方法都会被保留,即使某些方法从未被调用。这是因为它们可能通过接口被调用,或者通过 reflect 包动态调用。现在,编译器会丢弃所有不匹配接口的未导出方法。同样,如果程序中没有使用相应的 反射功能,链接器也可以丢弃其他仅通过反射可访问的已导出方法。这一变化使二进制文件缩小了 5-20%。
第三个变化是用于 reflect 包的运行时类型信息的更紧凑格式。最初设计编码格式是为了让运行时和 reflect 包中的解码器尽可能简单。通过使代码稍显晦涩难懂,我们可以在不影响 Go 程序运行时性能的情况下压缩格式。新格式使 Go 二进制文件进一步缩小了 5-15%。为 Android 构建的库和为 iOS 构建的归档文件缩小得更多,因为新格式包含的指针更少,每个指针都需要在位置无关代码中进行动态重定位。
此外,还有许多小的改进,例如改进的接口数据布局、更好的静态数据布局以及简化的依赖项。例如,HTTP 客户端不再链接整个 HTTP 服务器。完整的更改列表可以在 issue #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 字节。
移除调试信息的位置无关可执行文件对移动开发者尤为重要,因为这是 shipped 到手机上的程序类型。下载量过大对用户体验不好,因此这里的减小是一个好消息。
未来工作
对运行时类型信息的几项更改由于太晚而未能赶上 Go 1.7 的冻结,但希望能在 1.8 中实现,这将进一步减小程序的大小,尤其是位置无关程序。
所有这些更改都是保守的,它们减小了二进制文件的大小,而没有增加构建时间、启动时间、整体执行时间或内存使用量。我们可以采取更激进的措施来减小二进制文件的大小:用于压缩可执行文件的 upx 工具可以将二进制文件再缩小 50%,但代价是增加启动时间和可能增加内存使用量。对于极小的系统(比如那些可能挂在钥匙链上的设备),我们可以构建一个不包含反射的 Go 版本,尽管不清楚这样受限的语言是否足够有用。对于运行时中的某些算法,当每一千字节都很重要时,我们可以使用更慢但更紧凑的实现。所有这些都需要在后续的开发周期中进行更多研究。
感谢许多为使 Go 1.7 二进制文件更小做出贡献的贡献者!
下一篇文章: 使用子测试和子基准测试
上一篇文章: Go 1.7 发布
博客索引