1 of 34

图解Go之内存对齐

苗蕾 ( newbmiao)2020.4.2

2 of 34

提纲

  • 了解内存对齐的收益
  • 为什么要对齐
  • 怎么对齐:
    • 数据结构对齐
    • 内存地址对齐
  • 64位字的安全访问保证(32位平台)

3 of 34

了解内存对齐的收益

  • 提高代码平台兼容性
  • 优化数据对内存的使用
  • 避免一些内存不对齐带来的坑
  • 有助于一些源码的阅读

4 of 34

为什么要对齐

位 bit

计算机内部数据存储的最小单位

字节 byte

计算机数据处理的基本单位

机器字 machine word

计算机用来一次性处理事务的一个固定长度

5 of 34

为什么要对齐

1.平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.性能原因:

数据结构应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

6 of 34

为什么对齐 性能差异测试(无效)

不对齐的地址

7 of 34

为什么对齐 性能差异测试(无效

8 of 34

数据结构对齐 大小保证(size guarantee)

type

size in bytes

byte, uint8, int8

1

uint16, int16

2

uint32, int32, float32

4

uint64, int64, float64, complex64

8

complex128

16

struct{}, [0]T{}

0

9 of 34

数据结构对齐 对齐保证(align guarantee)

type

alignment guarantee

bool, byte, uint8, int8

1

uint16, int16

2

uint32, int32

4

float32, complex64

4

arrays

由其元素(element)类型决定

structs

由其字段(field)类型决定

other types

一个机器字(machine word)的大小

10 of 34

数据结构对齐工具

11 of 34

数据结构对齐 举个🌰

12 of 34

数据结构对齐 几个底层数据结构

13 of 34

数据结构对齐 举个特🌰:final zero field

14 of 34

数据结构对齐 重排优化(粗暴方式-按对齐值的递减来重排成员)

-40%

15 of 34

数据结构对齐 重排优化

16 of 34

数据结构对齐 内存对齐检测

github.com/NewbMiao/Dig101-Go/struct_align_demo.go

17 of 34

内存地址对齐

计算机结构可能会要求内存地址进行对齐;也就是说,一个变量的地址是一个因子的倍数,也就是该变量的类型是对齐值。

函数Alignof接受一个表示任何类型变量的表达式作为参数,并以字节为单位返回变量(类型)的对齐值。对于变量x:

https://golang.org/ref/spec#Package_unsafe

18 of 34

内存地址对齐 举个🌰

为什么是[3]uint32, 不是[12]byte

首先在64位系统和32位系统上,uint32能保证是4bytes对齐, 即state1地址是4N:

uintptr(unsafe.Pointer(&wg.state1))%4 == 0

而为保证8位对齐,我们只需要判断state1地址是否为8的倍数

如果是(N为偶数),那前8bytes就是64位对齐

否则(N为奇数),那后8bytes是64位对齐

而且剩余的4bytes可以给sema字段用,也不浪费内存

https://github.com/golang/go/issues/19149#issuecomment-347997080

19 of 34

64位字的安全访问保证(32位系统)

在x86-32上,64位函数使用Pentium MMX之前不存在的指令。

在非Linux ARM上,64位函数使用ARMv6k内核之前不可用的指令。

在ARM,x86-32和32位MIPS上,调用方有责任安排对原子访问的64位字的64位对齐。 变量或分配的结构、数组或切片中的第一个字(word)可以依赖当做是64位对齐的。

https://golang.org/pkg/sync/atomic/#pkg-note-BUG

20 of 34

64位字的安全访问保证 Why?

这是因为int64在bool之后未对齐。

它是32位对齐的,但不是64位对齐的,因为我们使用的是32位系统,

因此实际上只是两个32位值并排在一起。

https://github.com/golang/go/issues/6404#issuecomment-66085602

21 of 34

64位字的安全访问保证 How?

变量已分配的结构体、数组或切片中的第一个字(word)可以依赖当做是64位对齐的。

The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

已分配:new 或者make

22 of 34

64位字的安全访问保证 How?

23 of 34

64位字的安全访问保证 How?

24 of 34

64位字的安全访问保证 How?

25 of 34

64位字的安全访问保证 How?

26 of 34

64位字的安全访问保证 How?

27 of 34

64位字的安全访问保证 How?

28 of 34

一些源码中的🌰

GMP中的管理groutine本地队列的上下文p中,记录计时器运行时长的uint64,

需要保证32位系统上也是8byte对齐(原子操作)

29 of 34

一些源码中的🌰

堆对象分配的mheap中,管理全局cache的中心缓存列表central,分配或释放需要加互斥锁

另外为了不同列表间互斥锁不会伪共享,增加了cacheLinePadding

cacheLine 参考: https://appliedgo.net/concurrencyslower/

30 of 34

cacheLine引起的伪共享

31 of 34

64位字的安全访问保证 Bug!

如果包含首个64位字的结构体是12byte大小时,不一定能保证64未对齐

是tinyalloc分配小对象时没有做对齐保证

可以结合这里回想一下waitGroup.state1为什么不使用first word来保证64位字的安全访问)

32 of 34

64位字的安全访问保证 改为加锁!

33 of 34

总结

  • 内存对齐是为了cpu更高效访问内存中数据
  • 结构体对齐依赖类型的大小保证对齐保证
  • 地址对齐保证是:如果类型 t 的对齐保证是 n,那么类型 t 的每个值的地址在运行时必须是 n 的倍数。
  • struct内字段如果填充过多,可以尝试重排,使字段排列更紧密,减少内存浪费
  • 零大小字段要避免作为struct最后一个字段,会有内存浪费
  • 32位系统上对64位字的原子访问要保证其是8bytes对齐的;当然如果不必要的话,还是用加锁(mutex)的方式更清晰简单

34 of 34

Thanks

了解更多

Go 夜读

菜鸟Miao