很多Go开发者第一次听说“结构体对齐”,都会有一个疑问:“这不是编译器的事吗?跟我有什么关系?”,我今天告诉你:关系非常大。
一、什么是结构体对齐?
Go的结构体看起来很自由,字段想怎么写就怎么写。但在内存里,它们被摆放得非常“挑剔”。这种挑剔,叫做:结构体对齐(Struct Alignment)。
就像你每次搬家,收拾东西的纸箱子是不是能省就省?每个纸箱子里都会先装大件,再把小件放到边边角角里。最大化利用纸箱的空间。
二、为什么必须了解结构体对齐?
结构体对齐会直接影响:
-
结构体占用的内存大小
-
CPU访问效率
-
cache命中率
-
GC扫描成本
-
高并发场景下的整体性能
在以下场景中,它尤其重要:
-
高性能服务
-
大量结构体常驻内存
-
网络协议/RPC/游戏服务器
-
使用sync.Pool、unsafe、共享内存
三、编译器摆放的结构体
你写的结构体
type User struct {
Hobbies []string
Name string
Age int
}
在内存里的摆放,你以为:
[Hobbies][Name][Age]
其实是:
[Hobbies][padding][Name][padding][Age]
这些padding就是补齐,虽然看不见,但是真实存在。
四、为什么CPU不喜欢“随便放”的数据?
4.1 CPU访问内存不是“按字节”,而是按字。现代CPU访问内存有两个关键事实:
1. 按“字”访问
2. 对齐访问,速度更快
4.2 一个非常直观的比喻
内存是一排储物柜,int64是一件8格长的行李箱。
如果你从第一格开始放,刚刚好;
如果你从第二格开始放,会横跨两个柜子,取的时候要开两次门
CPU是个懒惰的家伙,它喜欢简单直接,一次能搞定的,为什么要搞两次。于是,编译器强制给你“对齐摆放”。
五、Go的结构体对齐规则
5.1 规则一:每个字段都有自己的对齐要求。常见类型的对齐值如下:
| 数据类型 | 大小(Size/Bytes) | 对齐值(Align/Bytes) | 备注 |
| bool,int8,uint8 | 1 | 1 | 最小单元,任意地址对齐 |
| int16,uint16 | 2 | 2 | 地址必须是2的倍数 |
| int32,uint32,float32 | 4 | 4 | 地址必须是4的倍数 |
| int64,uint64,float64 | 8 | 8 | 地址必须是8的倍数 |
| complex64 | 8 | 4 | 由两个float32组成 |
| complex128 | 16 | 8 | 由两个float64组成 |
| string | 16 | 8 | 内部包含1指针+1长度 |
| slice | 24 | 8 | 内部包含1指针+2长度 |
| pointer(如 *int) | 8 | 8 | 指针地址对齐 |
| interface{},any | 16 | 8 | 内部包含2个指针 |
5.2 规则二:字段的起始地址,必须是对齐值的倍数
type A struct{
a bool // 1 字节
b int64 // 8 字节
}
内存布局为:
offset 0: a (1 byte)
offset 1-7: padding
offset 8: b(8 bytes)
为了让b从8的倍数开始,Go插入了7字节的padding。
5.3 规则三:整个结构体的大小,也必须是最大对齐值的倍数
type B struct{
a int32
b int64
}
字段最大对齐值是8,所以struct总大小必须是8的倍数。这是为了数组场景:
arr := make([]B,10)
保证切片里每个元素里的b都是对齐的。
六、举个例子
type BadStruct struct {
a bool
b int64
c bool
}
字段大小:
a : 1
b : 8
c : 1
你以为是 10 字节
实际大小是:
内存布局解释如下:
a -> 1
padding -> 7
b -> 8
c -> 1
padding -> 7
-------------------
total -> 24
七、如何写一个CPU友好的结构体?
7.1 优化后
type GoodStruct struct {
b int64
a bool
c bool
}
优化后,结构体从24字节缩小到了16字节。如果这个结构体有100万个实例,常驻内存,直接省下了8MB内存。
7.2 结构体字段排序法则:从大到小排字段,请参照5.1的规则一里每个类型的大小和对齐值。例如:
1. 指针/int64/float64
2. int32
3. int16
4. bool/int8
这不是强制规范,但是对CPU友好,在性能场景中非常重要。
八、结构体对齐与cache的关系
CPU cache line通常是64字节。
如果你的结构体正好是64字节或者能被64整除,cache利用率最高。
九、结构体对齐与GC的隐形关系
Go的GC需要扫描指针,结构体越大,扫描成本越高。padding虽然不是指针,但它让结构体整体变大,间接增加了GC扫描成本,可以说结构体对齐的内存布局是GC性能的一部分。
一句话总结:Go的结构体不是按照你写的样子放,而是按照CPU喜欢的方式放。
十、为什么bool会浪费7个字节?请留言区作答
加班费计算器(vx小程序):
*源码地址*
1、公众号“Codee君”回复“每日一Go”获取源码
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!