结构体里字段的顺序会影响大小:每个字段要放在自己对齐的地址上,整个结构体的大小还要补齐到最大对齐。这篇用 reflect 打印每个字段的偏移与填充,对比两种字段顺序,量出差别,再用 fieldalignment 工具自动发现这类问题。
Bad:Sizeof = 32,Alignof = 8
a bool offset 0 size 1 align 1
b int64 offset 8 size 8 align 8 (前面填充 7 字节)
c bool offset 16 size 1 align 1
d int32 offset 20 size 4 align 4 (前面填充 3 字节)
e bool offset 24 size 1 align 1
(末尾填充 7 字节)
Good:Sizeof = 16,Alignof = 8
b int64 offset 0 size 8 align 8
d int32 offset 8 size 4 align 4
a bool offset 12 size 1 align 1
c bool offset 13 size 1 align 1
e bool offset 14 size 1 align 1
(末尾填充 1 字节)规则是两条:每个字段的偏移必须是它对齐值的整数倍;结构体的大小必须是它最大对齐值的整数倍。Bad 里 int64 前面的 7 字节、int32 前面的 3 字节、末尾 7 字节都是填充,合计 17 字节。把大对齐的字段放前面,小字段挤在一起,填充几乎消失。
bool Sizeof 1 Alignof 1
int16 Sizeof 2 Alignof 2
int32 Sizeof 4 Alignof 4
int64 Sizeof 8 Alignof 8
float32 Sizeof 4 Alignof 4
complex128 Sizeof 16 Alignof 8
string Sizeof 16 Alignof 8
[]int Sizeof 24 Alignof 8
any Sizeof 16 Alignof 8
map[int]int Sizeof 8 Alignof 8
[3]byte Sizeof 3 Alignof 1
struct{} Sizeof 0 Alignof 1对齐值就是类型里最大成员的对齐:string、切片、接口、map 都是 8 字节对齐(它们内部是指针与整数);complex128 是 16 字节但只要 8 字节对齐;数组的对齐等于元素的对齐;struct{} 大小是 0、对齐是 1。
ZeroTail:Sizeof = 16,Alignof = 8
a int64 offset 0 size 8 align 8
z struct {} offset 8 size 0 align 1
(末尾填充 8 字节)
ZeroHead:Sizeof = 8,Alignof = 8
z struct {} offset 0 size 0 align 1
a int64 offset 0 size 8 align 8空结构体字段放在结构体的最后会让整个结构体多出一个对齐单位(上面 8 → 16 字节),放在前面则没有代价。我没有读编译器里这条规则的源码(合理的解释是避免指向结构体末尾之后的指针,但这只是我的推测,没有验证)。
[]Bad 32 字节 × 1000000 = 30.5 MiB;[]Good 16 字节 × 1000000 = 15.3 MiB;省 50.0% 遍历 []Bad 求和 d 中位 603978.48 ns/op(最小 601124.72 / 最大 622435.33) 遍历 []Good 求和 d 中位 610044.56 ns/op(最小 589078.71 / 最大 613580.00)
切片元素大小减半,100 万个元素省了 15 MiB。但“只读一个字段、顺序遍历”的循环里耗时没有区别,所以这个例子里收益只是内存与 GC 扫描量;缓存压力更大的访问模式(随机访问、遍历很多字段)可能看到差别,本文没有测。
GOARCH=amd64 Bad Sizeof = 32,Alignof = 8,各字段偏移 [0 8 16 20 24] GOARCH=amd64 Good Sizeof = 16,Alignof = 8,各字段偏移 [0 8 12 13 14] GOARCH=arm64 Bad Sizeof = 32,Alignof = 8,各字段偏移 [0 8 16 20 24] GOARCH=arm64 Good Sizeof = 16,Alignof = 8,各字段偏移 [0 8 12 13 14] GOARCH=386 Bad Sizeof = 24,Alignof = 4,各字段偏移 [0 4 12 16 20] GOARCH=386 Good Sizeof = 16,Alignof = 4,各字段偏移 [0 8 12 13 14] GOARCH=arm Bad Sizeof = 24,Alignof = 4,各字段偏移 [0 4 12 16 20] GOARCH=arm Good Sizeof = 16,Alignof = 4,各字段偏移 [0 8 12 13 14]
386 与 arm 上 int64 只要 4 字节对齐,所以同一个结构体的大小与偏移不同。我用标准库 go/types 按 gc 编译器的规则计算,没有真的运行 32 位程序。要在 32 位平台上对 64 位字段做原子操作,还有额外的对齐要求(sync/atomic 文档里写明),本文没有验证。
go vet -vettool=fieldalignment:rc=1 p.go:3:10: Bad has size 32 but the optimal size is 16 leading to a waste of 16 bytes (50%) p.go:19:12: Mixed has size 56 (allocator size class 64) but the optimal size is 40 (allocator size class 48) leading to a waste of 16 bytes (25%)
fieldalignment 是 golang.org/x/tools 里的分析器,不在 go vet 默认的检查里,需要单独安装并作为 -vettool 运行。它不只比较字节数,还会提示分配器的 size class(56 字节的结构体分配时占 64 字节)。注意工具建议的字段顺序有时会让结构体更难读,是否值得要看这个类型有多少实例。