两个 Go Struct 可以包含完全相同的字段,但一个Struct 体可能比另一个Struct 体需要更多或更少的内存。

创建示例Struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

type City uint8

const (
NewYork City = iota
London
Paris
Mumbai
)

type Person struct {
currentResidence City
uniqueID int64
passportNumber int16
}

我声明了一些示例 City 常量,所以你可以看到如何使用该类型来表示世界上的四个不同位置。关键 iota 字只是确保每个常量都具有唯一的数值。

现在让我们声明一个 Person 变量,看看它需要多少内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

func main() {
me := Person{
currentResidence: London,
uniqueID: 9248511308,
passportNumber: 10564,
}

fmt.Printf(
"My Person struct uses %d bytes.\n",
unsafe.Sizeof(me),
)
}

当我运行这段代码时,它告诉我我们的 Person Struct 总共使用了 24 个字节来保存其数据。

然而,怎么会这样呢?

让我们一步一步地思考: uniqueID 字段是一个 64 位数字,因此它需要 8 个字节的存储,该 passportNumber 字段是一个 16 位数字,因此它只需要两个字节的存储, currentResidence 并且该字段是存储为 8 位数字的自定义 City 类型,只需要一个字节的存储。

(请注意,我们没有考虑字段的数值是有符号还是无符号,因为无符号整数在内存中占用的空间量与其对应的有符号整数完全相同,这就是为什么无符号整数的最大值总是大于有符号整数的最大值, 因为有符号整数已经使用一位来存储数字的符号。

如果我们将分别存储三个字段所需的存储量相加,我们只能得到 11 个字节(因为 8 + 2 + 1 == 11 )。

然而,正如我们刚刚看到的,该 Person Struct 似乎使用了两倍多的内存!

考虑数据对齐

当我们测量数据的大小时,我们以字节为单位进行讨论。

但是,计算机的 CPU 不会以字节为单位读取数据,而是以单词读取数据。

在 64 位系统上,一个字相当于 8 个字节。

在较旧的 32 位系统上,一个字相当于四个字节。

重要的是要记住,机器倾向于以多个单词读取数据,这就是解释为什么我们的 Person Struct 使用 24 个字节的原因。

它为每个字段使用一个单词,即使只有字段 uniqueID (64 位整数)充分利用了该单词。

我的机器有一个 64 位处理器,所以如果每个字都是 8 个字节,那么就很容易理解我们如何得到数字 24(因为 8 * 3 == 24 )。

停车位是固定大小的,但较小的汽车可以停在里面,就像大型公共汽车一样。同样,小字段可以像大字段一样放入内存中。

该 passportNumber 字段是一个 16 位整数,因此只需要两个字节的存储,它仍然可以从内存中访问整个字,但只使用其中的前四分之一,而后四分之三未使用。

同样, currentResidence 该字段是一种自定义 City 类型,最终等效于 8 位整数,实际上只需要一个字节的存储,但仍然从内存中访问整个字,并且根本不使用最后七个字节。

减少Struct 所需的内存量
我们无能为力来减少 uniqueID 字段的内存占用,因为它必须使用一个完整的单词,否则它将无法表示 64 位数字。

但是,您是否注意到如何将 passportNumber and currentResidence 字段放入一个单词中,因为它们之间只用完了三个字节?

事实上,我们可以强制 Go 编译器为这两个字段共享一个字的内存,只需在定义Struct Person 时对它们进行重新排序即可。

当我们在前面的示例中定义它时,我们将字段 uniqueID 放在其他两个字段之间,这意味着较小的字段不能共享内存,因为它们之间被一个完整的单词隔开。

另一方面,如果我们将 passportNumber 和 currentResidence 字段放在一起作为Struct 中的第一个或最后一个字段,那么我们突然允许它们共享内存,如下例所示:

1
2
3
4
5
6
7
package main 

type Person struct {
uniqueID int64
passportNumber int16
currentResidence City // remember that this is really a uint8
}

当我使用这个略微修改 Person 的定义运行我们之前编写的 main 函数时,我可以看到现在仅使用 16 字节的内存将相同的数据存储在我的机器上。

这仍然超过我们计算出的字段总共使用的 11 个字节的内存,但由于我们只能使用 8 个字节的倍数,即一个 64 位字,因此尽可能小,因为 8 个字节是不够的。

Person 这种Struct 的排序肯定比我们开始的更有效,而且,事实上,它现在已经尽可能地高效了。

对Struct 体的字段进行重新排序以减少其内存使用量正是我在这篇博文开头使用的术语Struct 打包的意思。

使用字段对齐工具

Go 团队开发了一种官方工具,该 fieldalignment 工具将在您的代码中搜索包含无法像其他方式那样高效排序的字段的Struct 。

可以通过运行以下命令来安装该工具:

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
然后,我们可以在 Go 文件或包上运行程序,方法是给它适当的路径,如下所示:

fieldalignment main.go
在我的初始示例代码上运行该 fieldalignment 工具将产生以下输出:

main.go:12:13: struct of size 24 could be 16
请注意,如果在运行程序时包含该 –fix 标志,如下所示,它将自动编辑 Go 文件,以便Struct 字段以最有效的顺序排列:

fieldalignment --fix main.go

使用这样的工具意味着您永远不必担心手动重新排序Struct 中的字段,以确保最有效地使用内存。