英文:
Reason for huge size of compiled executable of Go
问题
我编写了一个在我的Linux机器上生成本机可执行文件的Hello World Go程序。但是我对这个简单的Hello World Go程序的大小感到惊讶,它有1.9MB!
为什么Go语言的这样一个简单程序的可执行文件如此庞大?
英文:
I complied a hello world Go program which generated native executable on my linux machine. But I was surprised to see the size of the simple Hello world Go program, it was 1.9MB !
Why is it that the executable of such a simple program in Go is so huge?
答案1
得分: 114
这个确切的问题在官方FAQ中有提到:为什么我的简单程序会生成如此大的二进制文件?
引用答案:
gc工具链中的链接器(
5l
、6l
和8l
)进行静态链接。因此,所有的Go二进制文件都包含Go运行时,以及支持动态类型检查、反射甚至是panic时的堆栈跟踪所需的运行时类型信息。在Linux上,使用gcc编译和静态链接的一个简单的C语言“hello, world”程序大约为750 kB,其中包括了
printf
的实现。而一个等效的Go程序使用fmt.Printf
大约为1.9 MB,但其中包括了更强大的运行时支持和类型信息。
因此,你的Hello World的本地可执行文件大小为1.9 MB,因为它包含了一个运行时,提供了垃圾回收、反射和许多其他功能(尽管你的程序可能并没有真正使用这些功能,但它们确实存在)。还有你用来打印“Hello World”文本的fmt
包的实现(以及它的依赖项)。
现在试试以下操作:在你的程序中添加另一行fmt.Println("Hello World! Again")
,然后再次编译它。结果不会是2倍的1.9MB,而仍然只是1.9 MB!是的,因为所有使用的库(fmt
及其依赖项)和运行时已经添加到可执行文件中(所以只会添加一些额外的字节来打印你刚刚添加的第二个文本)。
英文:
This exact question appears in the official FAQ: Why is my trivial program such a large binary?
Quoting the answer:
>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.
So the native executable of your Hello World is 1.9 MB because it contains a runtime which provides garbage collection, reflection and many other features (which your program might not really use, but it's there). And the implementation of the fmt
package which you used to print the "Hello World"
text (plus its dependencies).
Now try the following: add another fmt.Println("Hello World! Again")
line to your program and compile it again. The result will not be 2x 1.9MB, but still just 1.9 MB! Yes, because all the used libraries (fmt
and its dependencies) and the runtime are already added to the executable (and so just a few more bytes will be added to print the 2nd text which you just added).
答案2
得分: 63
考虑以下程序:
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。
英文:
Consider the following program:
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
I get a a binary that is about 2 Mb in size.
The reason for this (which has been explained in other answers) is that we are using the "fmt" package which is quite large, but the binary has also not been stripped and this means that the symbol table is still there. If we instead instruct the compiler to strip the binary, it will become much smaller:
$ 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")
}
And then compile it:
$ 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.
答案3
得分: 17
请注意,二进制大小问题在golang/go项目的问题6853中进行了跟踪。
例如,提交a26c01a(适用于Go 1.4)将hello world减小了70kB:
> 因为我们不将这些名称写入符号表中。
考虑到编译器、汇编器、链接器和1.5版本的运行时将完全使用Go语言编写,您可以期待进一步的优化。
更新2016年Go 1.7:已经进行了优化,请参见“更小的Go 1.7二进制文件”。
但是在现在(2019年4月),占用最多空间的是runtime.pclntab
。
请参阅Raphael ‘kena’ Poss的“使用D3可视化Go可执行文件的大小:Go可执行文件的大小可视化”。
> 尽管没有太好的文档,但是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之间发生了什么的文档。简而言之:
>
> 在1.2之前,Go链接器会发出一个压缩的行表,程序会在运行时初始化时解压缩它。
>
> 在Go 1.2中,决定将行表在可执行文件中预先展开为最终格式,以便在运行时直接使用,而不需要额外的解压缩步骤。
>
> 换句话说,Go团队决定增加可执行文件的大小以节省初始化时间。
>
> 此外,从数据结构来看,它在编译后的二进制文件中的总大小与程序中函数的数量超线性相关,还与每个函数的大小有关。
英文:
Note that the binary size issue is tracked by issue 6853 in the golang/go project.
For instance, commit a26c01a (for Go 1.4) cut hello world by 70kB:
> because we don't write those names into the symbol table.
Considering the compiler, assembler, linker, and runtime for 1.5 will be
entirely in Go, you can expect further optimization.
Update 2016 Go 1.7: this has been optimized: see "Smaller Go 1.7 binaries".
But these day (April 2019), what takes the most place is runtime.pclntab
.
See "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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论