Go编译的可执行文件体积巨大的原因

Reason for huge size of compiled executable of Go

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

这么简单的Go程序,为什么可执行文件这么大?

请注意,二进制大小问题由 issue 6853 in the golang/go project 跟踪。

例如,commit a26c01a(对于 Go 1.4)将 hello world 减少 70kB:

because we don't write those names into the symbol table.

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


更新 2016 Go 1.7:已优化:参见“Smaller Go 1.7 binaries”。

但是最近(2019年4月),最占地方的是runtime.pclntab
参见“Why are my Go executable files so large? Size visualization of Go executables using D3" from Raphael ‘kena’ Poss.

It is not too well documented however this comment from the Go source code suggests its purpose:

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

The purpose of this data structure is to enable the Go runtime system to produce descriptive stack traces upon a crash or upon internal requests via the runtime.GetStack API.

So it seems useful. But why is it so large?

The URL https://golang.org/s/go12symtab hidden in the aforelinked source file redirects to a document that explains what happened between Go 1.0 and 1.2. To paraphrase:

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.

In other words, the Go team decided to make executable files larger to save up on initialization time.

Also, looking at the data structure, it appears that its overall size in compiled binaries is super-linear in the number of functions in the program, in addition to how large each function is.

这个问题出现在官方常见问题解答中:Why is my trivial program such a large binary?

引用答案:

The linkers in the gc tool chain (5l, 6l, and 8l) do static linking. All Go binaries therefore include the Go run-time, along with the run-time type information necessary to support dynamic type checks, reflection, and even panic-time stack traces.

A simple C "hello, world" program compiled and linked statically using gcc on Linux is around 750 kB, including an implementation of printf. An equivalent Go program using fmt.Printf is around 1.9 MB, but that includes more powerful run-time support and type information.

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

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

考虑以下程序:

package main

import "fmt"

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

如果我在我的 Linux AMD64 机器(Go 1.9)上构建它,像这样:

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

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

这样做的原因(已在其他答案中解释)是我们使用的 "fmt" 包非常大,但二进制文件也没有被剥离,这意味着符号 table 还在。如果我们改为指示编译器剥离二进制文件,它将变得更小:

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

但是,如果我们重写程序以使用内置函数 print,而不是 fmt.Println,如下所示:

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

我们最终得到了一个更小的二进制文件。这是我们在不借助 UPX 打包等技巧的情况下所能获得的最小值,因此 Go 运行时的开销大约为 700 Kb。