较小6%的GO二进制文件没有行号的生命周期

2020-07-22 03:42:24

如果你迫切想要更小6%的Go二进制文件,这篇博客文章是为你准备的。(我做这个实验是为了帮助解决Tailscale。披露:我是投资者。)。如果二进制大小不会让您担心,那么,也许您会觉得它很有趣。

为了获得这篇文章的示例编号,我从我的GOPATH中随机抓取了一件物品。这篇博文中所有的硬号码都是github.com/mvdan/sh/cmd/shfmt。从一些实验来看,它们似乎相当有代表性。

我使用GO工具链的Commit 9d812cfa5c作为我的基本提交。这是截至2020年4月29日的主分支;它可能类似于GO 1.15Beta1版本。我之所以使用它,而不是使用GO1.14,是因为它包含了几个二进制大小减少,其中一个是特别的,如果你关心二进制大小,你肯定会想要的。

有很多方法可以缩小二进制文件。删除无关紧要的依赖项可能是最好的方法。通过明智地使用syn.Once来避免全局地图可能会有所帮助。通过间接方式保持可分隔代码可能会有所帮助。您可以取消相等算法生成(单击…。直到你真正需要它为止)。您通常可以通过剥离调试信息来保存两位数的百分比:pass-ldflag=-w to go build。

让我们假设你已经做了所有这些事。而且你还需要缩水更多。而这种需求是如此迫切,你愿意为此做出一些牺牲。

GO二进制文件包含的不仅仅是可执行代码。有一些类型描述符描述围棋程序中的类型。存在垃圾收集数据结构。这里有调试器信息。并且存在从PC到位置信息的映射。(而且还有更多。)。

我们不能仅仅从二进制文件中完全去掉位置信息。那会打碎很多东西。

但是我们可以让所有的行号都一样。那应该不会打碎任何东西。毕竟,没有人(除了gofmt)说我们必须将代码放在多行中。

Go编译器和运行时必须做好准备,以便将大量内容放在一行上。

我们可以编写一个预处理器,也许可以使用-toolexec和//line指令,但是只需破解编译器就更容易了。幸运的是,这是分解良好的代码,因此我们只需要触及两个小点。

-a/src/cmd/编译/内部/语法/pos.go+b/src/cmd/pil.go@@-23,3+23,3@@type pos struct{//MakePos为给定的PosBase、行和列返回新的位置。-func MakePos(base*PosBase,line,coluint)pos{return pos{base,sat32(Line),sat32(Ol)}}+func MakePos(base*POSBase,line,coluint)pos{return pos{base,1,1}}@@-101,2+101,3@@type PosBase struct{func NewFileBase(Filename String)*PosBase{+filename="。

现在,每个文件都被命名为x.go,并且每个源位置都有行1和列1。(一旦您去除了Dwarf,列实际上对二进制大小并不重要。)。

这还不够。如果所有代码都位于x.go:1:1,那么工具链中还有另外两个不愉快的地方。

第一个是为调试器构建Dwarf。我们可以取消这个检查:我们已经在剥离矮人了,所以产生无效的矮人并不重要。

第二个是在CGO。有一些关于某些CGO杂注可以位于何处的安全检查。我们将相信自己不会违反它们(通过确保所有代码都使用未更改的工具链进行构建),并删除该安全检查。

我们的程序用-ldflag=-w编译,从3,126,800字节缩减到2,938,384字节,约占6%。

这主要是因为缩小了位置信息的编码。其中的一小部分来自编译器优化。

如果在这些文件中的每个文件上运行Go Tool Compile-S x.go,您将看到第一个程序包含对runtime.panicIndex的两个单独调用。第二个程序只包含一个这样的调用。原因是runtime.panicIndex必须显示包含死机行的行号的回溯。在第一个程序中,我们需要两个单独的恐慌,每个可能的恐慌行号对应一个。在第二个程序中,我们没有这样做,所以编译器将它们组合在一起。

由于我们现在将所有代码放在同一行,编译器可以比以前合并更多的异常。

我们这样做有什么损失呢?任何需要准确位置信息的东西。死机回溯仍将向您显示PC、函数、参数等。但是所有的行号都将是x。GO:1。只要有耐心,你仍然可以根据PC自己算出行号,但这需要一些手工工作。Pprof仍然可以按函数和指令分析性能,但它会认为所有事情都发生在同一行,这将使按行号分析变得毫无用处。

让我们来玩玩吧。如果我们只去掉文件名,而保留真实的行号,会怎么样?它只节省了0.9%。正如您所期望的那样,只保留准确的文件名并将所有行号设为1可以节省5.1%。

因此,节省的大部分资金来自于行号。如果我们保留原始文件名,并将所有行号截断为最接近的16的倍数,会怎么样?也就是说,将我们的差异缩小到:

-a/src/cmd/编译/内部/语法/pos.go+b/src/cmd/pil.go@@-23,3+23,3@@type pos struct{//MakePos为给定的PosBase、行和列返回新的位置。-func MakePos(base*PosBase,line,coluint)pos{return pos{base,1,1}}+func MakePos(base*PosBase,line,coluint)pos{return pos{base,sat32(line/16*16+1),1}}。

这将我们的二进制代码减少了2.2%。还不错。如果我们把所有行号除以16会怎么样?这保留了与截断完全相同的信息,但是我们必须手工相乘才能得到“附近”的行号。

-a/src/cmd/编译/内部/语法/pos.go+b/src/cmd/pil.go@@-23,3+23,3@@type pos struct{//MakePos为给定的PosBase、行和列返回新的位置。-func MakePos(base*PosBase,line,coluint)pos{return pos{base,1,1}}+func MakePos(base*PosBase,line,coluint)pos{return pos{base,sat32(line/16+1),1}}。

行号使用相对于前一个行号的可变编码存储在二进制中。较小的数字意味着较小的增量,因此可以更有效地存储。