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


90

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

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


22
巨大?我想您那时不会做太多Java!
Rick-777

19
好吧,我来自C / C ++背景!
凯西·饶

我刚刚尝试了这个scala原生的hello世界:scala-native.org/en/latest/user/sbt.html#minimal-sbt-project编译花费了很多时间,下载了很多东西,二进制文件是3.9 MB。
bli

我用2019年的发现更新了下面的答案
VonC

1
C#.NET Core 3.1中的简单Hello World应用程序dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=true生成一个约26MB的二进制文件!
贾拉勒

Answers:


90

这个确切的问题出现在官方常见问题解答中:为什么我的琐碎程序这么大的二进制文件?

引用答案:

在GC工具链(连接体5l6l8l)做静态链接。因此,所有Go二进制文件都包含Go运行时以及支持动态类型检查,反射甚至紧急时间堆栈跟踪所必需的运行时类型信息。

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

因此,Hello World的本机可执行文件为1.9 MB,因为它包含一个运行时,该运行时提供了垃圾回收,反射和许多其他功能(您的程序可能并未真正使用,但确实存在)。以及fmt用于打印"Hello World"文本的程序包的实现(及其依赖项)。

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


10
与glibc静态链接的AC“ hello world”程序为750K,因为glibc显然不是为静态链接而设计的,在某些情况下甚至无法正确进行静态链接。与musl libc静态链接的“ hello world”程序为14K。
克雷格·巴恩斯

我仍然在寻找,但是,很高兴知道链接的内容,这样攻击者也许就不会链接邪恶的代码。
理查德

那么,为什么Go运行时库不在DLL文件中,以便可以在所有Go exe文件之间共享呢?然后,“ hello world”程序可以像预期的那样只有几个KB,而不是2 MB。对于Windows上MSVC的其他绝佳替代方案,每个程序中都具有完整的运行时库是一个致命缺陷。
David Spector

我最好对我的评论提出异议:Go是“静态链接的”。好的,然后没有DLL。但是,静态链接并不意味着您需要链接(绑定)整个库,而只需链接库中实际使用的功能!
David Spector

43

考虑以下程序:

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”包很大,但是二进制文件也没有被剥离,这意味着符号表仍然存在。如果我们改为指示编译器剥离二进制文件,它将变得更小:

$ 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。


3
UPX压缩二进制文件,并在执行时即时对其进行解压缩。如果不解释它的作用,我不会忽略它,因为它在某些情况下很有用。二进制文件的大小有所减少,但以启动时间和RAM使用量为代价;此外,性能也会受到轻微影响。举个例子,可执行文件可以缩小到其(缩小的)大小的30%,并需要35毫秒的更长的运行时间。
simlev

10

请注意,golang / go项目中的问题6853跟踪了二进制大小问题。

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

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

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


Update 2016 Go 1.7:已优化:请参阅“ Small Go 1.7二进制文件”。

但是,这些天(2019年4月)占据了最大的位置runtime.pclntab
请参阅“所以去的大?大小可视化的可执行文件使用D3为什么我转到可执行文件从”拉斐尔“凯纳” POSS

它的文档记录不是很好,但是Go源代码中的注释说明了其目的:

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

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

因此,它似乎很有用。但是为什么这么大呢?

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

在1.2之前的版本中,Go链接器发出了一个压缩的线表,程序会在运行时初始化时对其进行解压缩。

在Go 1.2中,决定将可执行文件中的行表预展开为最终格式,以适合在运行时直接使用,而无需执行额外的解压缩步骤。

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

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

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


2
我看不出他的实现语言与它有什么关系。他们需要使用共享库。令人难以置信的是,他们现在已经不存在了。
罗恩侯爵

2
@EJP:为什么他们需要使用共享库?
Flimzy

9
@ EJP,Go的简单之处在于不使用共享库。实际上,Go根本没有任何依赖关系,它使用普通的系统调用。只需部署一个二进制文件即可。否则会严重损害语言及其生态系统。
creker

10
具有静态链接的二进制文件经常被遗忘的方面是,它使得可以在完全空的Docker容器中运行它们。从安全角度来看,这是理想的。当容器为空时,您可以闯入(如果静态链接的二进制文件有缺陷),但是由于在容器中找不到任何内容,因此攻击会在那里停止。
2017年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.