通过禁止比较让 Go 二进制文件变小
创始人
2024-03-02 04:58:29
0

大家常规的认知是,Go 程序中声明的类型越多,生成的二进制文件就越大。这个符合直觉,毕竟如果你写的代码不去操作定义的类型,那么定义一堆类型就没有意义了。然而,链接器的部分工作就是检测没有被程序引用的函数(比如说它们是一个库的一部分,其中只有一个子集的功能被使用),然后把它们从最后的编译产出中删除。常言道,“类型越多,二进制文件越大”,对于多数 Go 程序还是正确的。

本文中我会深入讲解在 Go 程序的上下文中“相等”的意义,以及为什么像这样的修改会对 Go 程序的大小有重大的影响。

定义两个值相等

Go 的语法定义了“赋值”和“相等”的概念。赋值是把一个值赋给一个标识符的行为。并不是所有声明的标识符都可以被赋值,如常量和函数就不可以。相等是通过检查标识符的内容是否相等来比较两个标识符的行为。

作为强类型语言,“相同”的概念从根源上被植入标识符的类型中。两个标识符只有是相同类型的前提下,才有可能相同。除此之外,值的类型定义了如何比较该类型的两个值。

例如,整型是用算数方法进行比较的。对于指针类型,是否相等是指它们指向的地址是否相同。映射和通道等引用类型,跟指针类似,如果它们指向相同的地址,那么就认为它们是相同的。

上面都是按位比较相等的例子,即值占用的内存的位模式是相同的,那么这些值就相等。这就是所谓的 memcmp,即内存比较,相等是通过比较两个内存区域的内容来定义的。

记住这个思路,我过会儿再来谈。

结构体相等

除了整型、浮点型和指针等标量类型,还有复合类型:结构体。所有的结构体以程序中的顺序被排列在内存中。因此下面这个声明:

type S struct {
    a, b, c, d int64
}

会占用 32 字节的内存空间;a 占用 8 个字节,b 占用 8 个字节,以此类推。Go 的规则说如果结构体所有的字段都是可以比较的,那么结构体的值就是可以比较的。因此如果两个结构体所有的字段都相等,那么它们就相等。

a := S{1, 2, 3, 4}
b := S{1, 2, 3, 4}
fmt.Println(a == b) // 输出 true

编译器在底层使用 memcmp 来比较 a 的 32 个字节和 b 的 32 个字节。

填充和对齐

然而,在下面的场景下过分简单化的按位比较的策略会返回错误的结果:

type S struct {
    a byte
    b uint64
    c int16
    d uint32
}

func main()
    a := S{1, 2, 3, 4}
    b := S{1, 2, 3, 4}
    fmt.Println(a == b) // 输出 true
}

编译代码后,这个比较表达式的结果还是 true,但是编译器在底层并不能仅依赖比较 ab 的位模式,因为结构体有填充

Go 要求结构体的所有字段都对齐。2 字节的值必须从偶数地址开始,4 字节的值必须从 4 的倍数地址开始,以此类推 1 。编译器根据字段的类型和底层平台加入了填充来确保字段都对齐。在填充之后,编译器实际上看到的是 2 :

type S struct {
    a byte
    _ [7]byte // 填充
    b uint64
    c int16
    _ [2]int16 // 填充
    d uint32
}

填充的存在保证了字段正确对齐,而填充确实占用了内存空间,但是填充字节的内容是未知的。你可能会认为在 Go 中 填充字节都是 0,但实际上并不是 — 填充字节的内容是未定义的。由于它们并不是被定义为某个确定的值,因此按位比较会因为分布在 s 的 24 字节中的 9 个填充字节不一样而返回错误结果。

Go 通过生成所谓的相等函数来解决这个问题。在这个例子中,s 的相等函数只比较函数中的字段略过填充部分,这样就能正确比较类型 s 的两个值。

类型算法

呵,这是个很大的设置,说明了为什么,对于 Go 程序中定义的每种类型,编译器都会生成几个支持函数,编译器内部把它们称作类型的算法。如果类型是一个映射的键,那么除相等函数外,编译器还会生成一个哈希函数。为了维持稳定,哈希函数在计算结果时也会像相等函数一样考虑诸如填充等因素。

凭直觉判断编译器什么时候生成这些函数实际上很难,有时并不明显,(因为)这超出了你的预期,而且链接器也很难消除没有被使用的函数,因为反射往往导致链接器在裁剪类型时变得更保守。

通过禁止比较来减小二进制文件的大小

现在,我们来解释一下 Brad 的修改。向类型添加一个不可比较的字段 3 ,结构体也随之变成不可比较的,从而强制编译器不再生成相等函数和哈希函数,规避了链接器对那些类型的消除,在实际应用中减小了生成的二进制文件的大小。作为这项技术的一个例子,下面的程序:

package main

import "fmt"

func main() {
    type t struct {
        // _ [0][]byte // 取消注释以阻止比较
        a byte
        b uint16
        c int32
        d uint64
    }
    var a t
    fmt.Println(a)
}

用 Go 1.14.2(darwin/amd64)编译,大小从 2174088 降到了 2174056,节省了 32 字节。单独看节省的这 32 字节似乎微不足道,但是考虑到你的程序中每个类型及其传递闭包都会生成相等和哈希函数,还有它们的依赖,这些函数的大小随类型大小和复杂度的不同而不同,禁止它们会大大减小最终的二进制文件的大小,效果比之前使用 -ldflags="-s -w" 还要好。

最后总结一下,如果你不想把类型定义为可比较的,可以在源码层级强制实现像这样的奇技淫巧,会使生成的二进制文件变小。


附录:在 Brad 的推动下,Cherry ZhangKeith Randall 已经在 Go 1.15 做了大量的改进,修复了最严重的故障,消除了无用的相等和哈希函数(虽然我猜想这也是为了避免这类 CL 的扩散)。

相关文章:

  1. Go 运行时如何高效地实现映射(不使用泛型)
  2. 空结构体
  3. 填充很难
  4. Go 中有类型的 nil(2)

  1. 在 32 位平台上 int64unit64 的值可能不是按 8 字节对齐的,因为平台原生的是以 4 字节对齐的。查看 议题 599 了解内部详细信息。
  2. 32 位平台会在 ab 的声明中填充 _ [3]byte。参见前一条。
  3. Brad 使用的是[0]func(),但是所有能限制和禁止比较的类型都可以。添加了一个有 0 个元素的数组的声明后,结构体的大小和对齐不会受影响。

via: https://dave.cheney.net/2020/05/09/ensmallening-go-binaries-by-prohibiting-comparisons

作者:Dave Cheney 选题:lujun9972 译者:lxbwolf 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

相关内容

“漂亮饭”占领三里屯
文|潮汐商业评论 听说三里屯新开了不少漂亮饭餐厅,生意很火,想坐...
2025-07-02 19:11:33
Google 这款免费 A...
在不久前结束的 Google I/O 上,Google 悄咪咪地在...
2025-06-16 07:41:52
谷歌突袭发布AI应用,无需...
本播客由扣子空间(coze.cn)一键生成 整理|冬梅 近日,据...
2025-06-02 16:42:20
Centos7 XFS(d...
在k8s的道路上我们都是小白,每天启动虚机都会遇到各...
2025-06-01 19:21:59
Golang每日一练(le...
目录 34. 查找元素首末位置 Find-first-and-la...
2025-06-01 07:25:54
【Java|golang】...
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 n...
2025-06-01 01:57:50

热门资讯

Helix:高级 Linux ... 说到 基于终端的文本编辑器,通常 Vim、Emacs 和 Nano 受到了关注。这并不意味着没有其他...
使用 KRAWL 扫描 Kub... 用 KRAWL 脚本来识别 Kubernetes Pod 和容器中的错误。当你使用 Kubernet...
JStock:Linux 上不... 如果你在股票市场做投资,那么你可能非常清楚投资组合管理计划有多重要。管理投资组合的目标是依据你能承受...
通过 SaltStack 管理... 我在搜索Puppet的替代品时,偶然间碰到了Salt。我喜欢puppet,但是我又爱上Salt了:)...
Epic 游戏商店现在可在 S... 现在可以在 Steam Deck 上运行 Epic 游戏商店了,几乎无懈可击! 但是,它是非官方的。...
《Apex 英雄》正式可在 S... 《Apex 英雄》现已通过 Steam Deck 验证,这使其成为支持 Linux 的顶级多人游戏之...
如何在 Github 上创建一... 学习如何复刻一个仓库,进行更改,并要求维护人员审查并合并它。你知道如何使用 git 了,你有一个 G...
2024 开年,LLUG 和你... Hi,Linuxer,2024 新年伊始,不知道你是否已经准备好迎接新的一年~ 2024 年,Lin...
什么是 KDE Connect... 什么是 KDE Connect?它的主要特性是什么?它应该如何安装?本文提供了基本的使用指南。科技日...
Opera 浏览器内置的 VP... 昨天我们报道过 Opera 浏览器内置了 VPN 服务,用户打开它可以防止他们的在线活动被窥视。不过...