【问题标题】:Reason for huge size of compiled executable of GoGo 编译可执行文件巨大的原因
【发布时间】:2015-02-18 04:47:10
【问题描述】:

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

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

【问题讨论】:

  • 嗯,我来自 C/C++ 背景!
  • 我刚刚尝试了这个scala-native hello world:scala-native.org/en/latest/user/sbt.html#minimal-sbt-project 编译花了很长时间,下载了很多东西,二进制文件是3.9MB。
  • 我已经用 2019 年的调查结果更新了 my answer below
  • 在 C# .NET Core 3.1 中使用 dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=true 的简单 Hello World 应用程序会生成一个约 26MB 的二进制文件!
  • @Jalal 为什么?这是一个巨大的差异。你知道为什么 .NET Core 可执行文件如此庞大吗?

标签: go executable


【解决方案1】:

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

引用答案:

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

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

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

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

【讨论】:

  • 一个 C "hello world" 程序,与 glibc 静态链接是 750K,因为 glibc 明确不是为静态链接而设计的,在某些情况下甚至不可能正确静态链接。与 musl libc 静态链接的“hello world”程序为 14K。
  • 我还在寻找,但是,如果知道链接的内容会很好,这样攻击者可能没有链接恶意代码。
  • @DavidSpector Go 运行时是一个复杂的机器:内存分配器、GC、调度器......你不能只是“摇动那棵树”。
  • @DavidSpector 静态链接它可以防止您遇到运行时版本更改和破坏程序的问题。此外,它还使交叉编译变得更加容易,因为您不必安装所有系统库就可以在不同的架构上编译您的 go 程序。这些当然是权衡取舍,但如果您考虑到 1.9MB 作为硬盘空间的成本,甚至通过 Internet 传输的成本,这并不多,这是完全值得的。
  • @DavidSpector 是的,“hello world”可能不会使用添加到二进制文件中的“花哨”功能,但“hello world”通常不是您愿意编写的应用程序。因此,从“hello world”优化未使用的运行时特性根本不值得。任何体面的应用都会(直接或间接)使用反射、gc、调度程序等。
【解决方案2】:

考虑以下程序:

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。

【讨论】:

  • UPX 压缩二进制文件并在执行时即时解压缩它们。如果不解释它的作用,我不会忽视它,因为它在某些情况下很有用。以牺牲启动时间和 RAM 使用为代价来减少二进制大小;此外,性能也会受到轻微影响。举个例子,一个可执行文件可以缩小到其(剥离)大小的 30%,运行时间要长 35 毫秒。
  • 我用 go build -ldflags "-s -w" 编译了我的项目,它只是将二进制文件从 6.7MB 减少到 5.2MB
  • 所以 IMO 连同符号可能还有其他导致问题的因素。
  • Go 编译器生成的二进制文件不仅包含代码。它们还包含类型信息(用于反射)和一个将地址映射到函数名称的块(为了产生可读的堆栈跟踪,例如在恐慌时)。此信息是 Go 运行时需要的,不能从二进制文件中剥离,并且与调试符号无关。这就是 Go 二进制文件比 C 二进制文件大得多的主要原因。
【解决方案3】:

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

例如,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”。

它的文档记录不是很好,但是 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 团队决定增大可执行文件以节省初始化时间。

此外,查看数据结构,除了每个函数的大小之外,它在编译二进制文件中的总体大小似乎与程序中的函数数量呈超线性关系。

【讨论】:

  • 我看不出他的实现语言与它有什么关系。他们需要使用共享库。令人难以置信的是,他们在这个时代还没有。
  • @EJP:他们为什么需要使用共享库?
  • @EJP,Go 的简单部分在于不使用共享库。事实上,Go 根本没有任何依赖关系,它使用普通的系统调用。只需部署一个二进制文件,它就可以工作。否则会严重损害语言及其生态系统。
  • 静态链接二进制文件的一个经常被遗忘的方面是它可以在完全空的 Docker 容器中运行它们。从安全的角度来看,这是理想的。当容器为空时,你也许可以闯入(如果静态链接的二进制文件有缺陷),但由于容器中没有任何内容,攻击就停止了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-04
  • 1970-01-01
相关资源
最近更新 更多