每日一Go-45、什么是Go语言的结构体对齐?为什么要对齐?

0 阅读4分钟

很多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,uint811最小单元,任意地址对齐
int16,uint1622地址必须是2的倍数
int32,uint32,float3244地址必须是4的倍数
int64,uint64,float6488地址必须是8的倍数
complex6484由两个float32组成
complex128168由两个float64组成
string168内部包含1指针+1长度
slice248内部包含1指针+2长度
pointer(如 *int)88指针地址对齐  
interface{},any168内部包含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小程序):

微信搜索“加班计”.png

*源码地址*

1、公众号“Codee君”回复“每日一Go”获取源码

2、pan.baidu.com/s/1B6pgLWfS…


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!