已编译的 Go 可执行文件大的原因

我编写了一个 hello world Go 程序,它在我的 linux 机器上生成了本地可执行文件。但是我很惊讶地看到这个简单的 Hello world Go 程序的大小是1.9 MB!

为什么这样一个简单的程序在 Go 中的可执行文件如此之大?

39690 次浏览

请注意,Golang/Go 项目中的 第6853期跟踪二进制大小问题。

例如,提交26c01a(对于 Go 1.4) 将 hello world 减少70kB:

因为我们不会把这些名字写进符号表。

考虑到1.5的编译器、汇编器、链接器和运行时将是 完全在围棋中,您可以期待进一步的优化。


更新2016 Go 1.7: 这已经被优化了: 见“ Smaller Go 1.7 binaries”。

但是现在(2019年4月) ,占据最多位置的是 强 > runtime.pclntab
See "为什么我的 Go 可执行文件这么大? 使用 D3的 Go 可执行文件的大小可视化" from Raphael ‘kena’ Poss.

虽然没有很好的文档记录,但是这条来自 Go 源代码的评论暗示了它的目的:

// A LineTable is a data structure mapping program counters to line numbers.

这种数据结构的目的是使 Go 运行时系统能够在崩溃时或通过 runtime.GetStack API 在内部请求时生成描述性堆栈跟踪。

所以它看起来很有用,但是为什么它这么大呢?

前面链接的源文件中隐藏的 URL https://golang.org/s/go12symtab重定向到一个文档,该文档解释了 Go 1.0和1.2之间发生的情况。转述:

prior to 1.2, the Go linker was emitting a compressed line table, and the program would decompress it upon initialization at run-time.

in Go 1.2, a decision was made to pre-expand the line table in the executable file into its final format suitable for direct use at run-time, without an additional decompression step.

换句话说,Go 团队决定将可执行文件放大以节省初始化时间。

另外,从数据结构来看,除了每个函数的大小之外,编译后的二进制文件中的数据结构的总体大小在程序中的函数数量上似乎是超线性的。

https://science.raphael.poss.name/go-executable-size-visualization-with-d3/size-demo-ss.png

这个问题出现在官方的常见问题解答: 为什么我的微不足道的程序是这么大的二进制文件

引用答案:

Gc 工具链(5l6l8l)中的链接器执行静态链接。因此,所有 Go 二进制文件都包含 Go 运行时,以及支持动态类型检查、反射、甚至应急时堆栈跟踪所需的执行期型态讯息。

在 Linux 上使用 gcc 编译和静态链接的简单 C“ hello,world”程序大约有750kB,包括 printf的实现。使用 fmt.Printf的同等 Go 程序大约有1.9 MB,但是它包含了更强大的运行时支持和类型信息。

所以 Hello World 的本地可执行文件是1.9 MB,因为它包含一个提供垃圾收集、反射和许多其他特性的运行时(你的程序可能不会真正使用这些特性,但它确实存在)。以及用于打印 "Hello World"文本(及其依赖项)的 fmt包的实现。

现在尝试以下操作: 在程序中添加另一行 fmt.Println("Hello World! Again")并再次编译它。结果不会是2x1.9 MB,但仍然只有1.9 MB!是的,因为所有使用过的库(fmt及其依赖项)和运行时都已经添加到可执行文件中(所以只需要添加几个字节就可以打印刚才添加的第二个文本)。

考虑以下方案:

package main


import "fmt"


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

If I build this on my Linux AMD64 machine (Go 1.9), like this:

$ go build
$ ls -la helloworld
-rwxr-xr-x 1 janf group 2029206 Sep 11 16:58 helloworld

我得到一个大约2Mb 的二进制文件。

这样做的原因(已经在其他答案中解释过了)是因为我们使用的“ fmt”包相当大,但是二进制文件也没有被剥离,这意味着符号表仍然存在。如果我们指示编译器去掉二进制文件,它将变得小得多:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 1323616 Sep 11 17:01 helloworld

However, if we rewrite the program to use the builtin function print, instead of fmt.Println, like this:

package main


func main() {
print("Hello World!\n")
}

然后编译它:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 714176 Sep 11 17:06 helloworld

We end up with an even smaller binary. This is as small as we can get it without resorting to tricks like UPX-packing, so the overhead of the Go-runtime is roughly 700 Kb.