Go编译后可执行文件大小巨大的原因是什么?

128

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

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


31
嗯,我有C/C++的背景! - Karthic Rao
1
我刚刚尝试了这个Scala-Native的Hello World示例:http://www.scala-native.org/en/latest/user/sbt.html#minimal-sbt-project。编译花费了相当长的时间,下载了很多东西,生成的二进制文件大小为3.9MB。 - bli
我已经更新了下面的答案(https://dev59.com/3V4b5IYBdhLWcg3w5VE9#28577424),并附上了2019年的发现。 - VonC
3
使用C# .NET Core 3.1编写的简单“Hello World”应用程序,通过dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=true命令生成一个大小约为26MB的二进制文件! - Jalal
@ZackISSOIR 这是因为 "dotnet publish -r win-x64" 是一个自包含部署,这意味着它将部署整个运行时和您的代码。如果您添加 "self-contained false",则它将是框架相关的,并且只会部署您的代码。在这种情况下,控制台的 hello world 只有 124k。 - Jr_
显示剩余3条评论
3个回答

114
This exact question appears in the official FAQ: 为什么我的简单程序会生成如此庞大的二进制文件? 引用答案:
链接器在gc工具链中(5l6l8l)执行静态链接。因此,所有Go二进制文件都包含Go运行时,以及支持动态类型检查、反射甚至panic时堆栈跟踪所需的运行时类型信息。
在Linux上,使用gcc编译和静态链接的一个简单的C语言“hello world”程序大小约为750kB,其中包括printf的实现。相应地,使用fmt.Printf的等效Go程序大约为1.9MB,但这包括更强大的运行时支持和类型信息。
因此,你的Hello World本地可执行文件大小为1.9MB,因为它包含了提供垃圾回收、反射和许多其他功能的运行时(虽然你的程序可能并没有真正使用这些功能,但它们确实存在),以及你用于打印"Hello World"文本的fmt包的实现(以及它的依赖项)。
现在请尝试以下操作:向您的程序中添加另一行fmt.Println("Hello World! Again")并重新编译。结果不会是2倍的1.9MB,而仍然只有1.9MB!是的,因为所有使用的库(fmt及其依赖项)和运行时已经添加到可执行文件中(所以只需添加几个字节即可打印您刚刚添加的第二个文本)。

23
使用glibc静态链接的一个“hello world”程序大小为750K,因为glibc并非专门设计用于静态链接,并且在某些情况下甚至无法正确地进行静态链接。而使用musl libc静态链接的“hello world”程序大小为14K。 - Craig Barnes
2
@DavidSpector Go运行时是一台复杂的机器:内存分配器、垃圾回收器、调度器... 你不能只是“摇晃那棵树”。 - jub0bs
2
@DavidSpector 静态链接可以避免运行时版本更改导致程序出现问题。此外,它使跨编译变得更加容易,因为您不必安装所有系统库即可在不同的架构上编译go程序。当然,这些都是权衡考虑,但如果您考虑1.9MB在硬盘空间或甚至在互联网上传输的成本不高,那么这完全值得。 - CodeMonkey
3
@DavidSpector 是的,“Hello World”可能不使用二进制文件中添加的“花哨”功能,但是“Hello World”通常不是您想要编写的应用程序。因此,优化“Hello World”中未使用的运行时功能根本不值得。任何体面的应用程序都将使用(直接或间接地)反射、垃圾回收、调度器等。 - icza
1
这意味着Go程序不依赖于任何libc,因为所有依赖都是静态链接的? - Ivan Montilla
显示剩余5条评论

64

考虑以下程序:

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

我收到了一个大约2MB大小的二进制文件。

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

$ 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运行时的开销大约为700Kb。


6
UPX压缩二进制文件,在执行时动态解压缩。它可以在某些情况下非常有用,因此不要轻易将其视为一种诡计而不加说明。二进制文件的大小会略微减小,但会增加启动时间和RAM使用量;此外,性能也可能受到轻微影响。例如,一个可执行文件的大小可以缩小到其(剥离后)大小的30%,但运行时间会延长35毫秒。 - simlev
我使用"go build -ldflags"-s-w"编译了我的项目,它将二进制文件从6.7MB减小到了5.2MB。 - Samir Kape
在我看来,除了符号之外,可能还有其他因素导致问题。 - Samir Kape
1
Go编译器生成的二进制文件不仅包含代码,还包括类型信息(用于反射)以及将地址映射到函数名的块(为了生成可读的堆栈跟踪,例如当发生panic时)。这些信息是Go运行时所需的,无法从二进制文件中剥离,并且与调试符号无关。这就是为什么Go二进制文件比C二进制文件要大得多的主要原因。 - Joppe

17
请注意,二进制文件大小问题由golang/go项目中的issue 6853跟踪。
例如,commit a26c01a(适用于Go 1.4)通过以下方式减少了hello world约70kB

因为我们不将这些名称写入符号表中。

考虑到编译器、汇编器、链接器和运行时在1.5中将完全使用Go语言编写,您可以期待进一步的优化。
更新 2016 Go 1.7: 这已经被优化了:请参见 "Smaller Go 1.7 binaries"。
但是现在(2019年4月),最占用空间的是 runtime.pclntab。请参见Raphael ‘kena’ Poss的 "Why are my Go executable files so large? Size visualization of Go executables using D3"。

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.

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


2
我不明白实现语言和这有什么关系。他们需要使用共享库。在当今时代,他们居然还没有使用共享库,有点难以置信。 - user207421
3
为什么他们需要使用共享库? - Jonathan Hall
13
@EJP,Go语言的简单性部分在于不使用共享库。事实上,Go根本没有任何依赖,它使用纯系统调用。只需部署一个单一的二进制文件,它就可以正常工作。如果情况不同,将会严重损害该语言及其生态系统。 - creker
16
使用静态链接二进制文件的一个经常被忽视的方面是,它使得在完全空白的Docker容器中运行它们成为可能。从安全角度来看,这是理想的。当容器为空时,如果静态链接的二进制文件存在漏洞,攻击者可能会入侵,但由于容器中没有可以发现的东西,攻击只会止步于此。 - Joppe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接