Categories
程式開發

golang内存对齐


0x00 面试题

下面是之前的小弟去面试腾讯电竞碰到的面试题:

type S struct {
A uint32
B uint64
C uint64
D uint64
E struct{}
}

上面的struct S,占用多大的内存?首先,我们可以明确S的是8字节对齐的,因此我给出的答案是32。但是很明显,答案并不是32。先看下正确答案:

func main() {
fmt.Println(unsafe.Offsetof(S{}.E))
fmt.Println(unsafe.Sizeof(S{}.E))
fmt.Println(unsafe.Sizeof(S{}))
}

终端输出:

32
0
40

可以看到,S.E的偏移量offset是32,并且size确实是0,但是S实例的size却是40。说明S.E后面隐藏着一个8字节的padding。

0x01 社区解答

那为什么需要这个padding呢?在github上面查到了相关的issue“,看见社区大佬的回复是这样的:

Trailing zero-sized struct fields are padded because if they weren’t, &C.E would point to an invalid memory location.

结构体尾部size为0的变量(字段)会被分配内存空间进行填充,原因是如果不给它分配内存,该变量(字段)指针将指向一个非法的内存空间(类似C/C++的野指针)。

并且还给了个相关的issue“地址:

If a non-zero-size struct contains a final zero-size field f, the address &x.f may point beyond the allocation for the struct. This could cause a memory leak or a crash in the garbage collector (invalid pointer found).

一个非空结构体包含有尾部size为0的变量(字段),如果不给它分配内存,那么该变量(字段)的指针地址将指向一个超出该结构体内存范围的内存空间。这可能会导致内存泄漏,或者在内存垃圾回收过程中,程序crash掉。

0x02 原理

1. 术语

字(word)

是用于表示其自然的数据单位,也叫machine word。字是电脑用来一次性处理事务的一个固定长度。

字长

一个字的位数,现代电脑的字长通常为 16、32、64 位。(一般 N 位系统的字长是 N/8 字节。)

2. 为什么要对齐

操作系统并非一个字节一个字节访问内存,而是按2, 4, 8这样的字长来访问。因此,当CPU从存储器读数据到寄存器,或者从寄存器写数据到存储器,IO的数据长度通常是字长。如 32 位系统访问粒度是 4 字节(bytes),64 位系统的是 8 字节。

当被访问的数据长度为 n 字节且该数据地址为n字节对齐,那么操作系统就可以高效地一次定位到数据,无需多次读取、处理对齐运算等额外操作。

数据结构应该尽可能地在自然边界上对齐。如果访问未对齐的内存,CPU需要做两次内存访问。

3. 数据结构对齐

看下go官方文档 Size and alignment guarantees” 对于go数据类型的大小保证和对齐保证:

golang内存对齐 1

大小保证

在Go中,如果两个值的类型为同一种类的类型,并且它们的类型的种类不为字符串、接口、数组和结构体,则这两个值的尺寸总是相等的。

目前(Go 1.14),至少对于官方标准编译器来说,任何一个特定类型的所有值的尺寸都是相同的。所以我们也常说一个值的尺寸为此值的类型的尺寸。

下表列出了各种种类的类型的尺寸(对标准编译器1.14来说):

golang内存对齐 2

一个结构体类型的尺寸取决于它的各个字段的类型尺寸和这些字段的排列顺序。 为了程序执行性能,编译器需要保证某些类型的值在内存中存放时必须满足特定的内存地址对齐要求。 地址对齐可能会造成相邻的两个字段之间在内存中被插入填充一些多余的字节。 所以,一个结构体类型的尺寸必定不小于(常常会大于)此结构体类型的各个字段的类型尺寸之和。

一个数组类型的尺寸取决于它的元素类型的尺寸和它的长度。它的尺寸为它的元素类型的尺寸和它的长度的乘积。

struct{} 和[0]T{} 的大小为 0; 不同的大小为 0 的变量可能指向同一块地址。

对齐保证

go官方文档中的对对齐保证的要求只有如下解释:

对于任何类型的变量x,unsafe.Alignof(x)的结果最小为1。对于一个结构体类型的变量x,unsafe.Alignof(x)的结果为x的所有字段的对齐保证unsafe.Alignof(x.f)中的最大值(但是最小为1)。对于一个数组类型的变量x,unsafe.Alignof(x)的结果和此数组的元素类型的一个变量的对齐保证相等。

golang内存对齐 3

类型对齐保证也称为值地址对齐保证。 如果一个类型T的对齐保证为N(一个正整数),则在运行时刻T类型的每个(可寻址的)值的地址都是N的倍数。 我们也可以说类型T的值的地址保证为N字节对齐的。

事实上,每个类型有两个对齐保证。当它被用做结构体类型的字段类型时的对齐保证称为此类型的字段对齐保证,其它情形的对齐保证称为此类型的一般对齐保证。

对于一个类型T,我们可以调用unsafe.Alignof

// class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型
// bytes/obj: 该class代表对象的字节数
// bytes/span: 每个span占用堆的字节数,也即页数*页大小
// objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)
// waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)

// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52% 8192-(48*170)
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%

每个mspan按照它自身的属性Size Class的大小分割成若干个object,每个object可存储一个对象。并且会使用一个位图来标记其尚未使用的object。属性Size Class决定object大小,而mspan只会分配给和object尺寸大小接近的对象,当然,对象的大小要小于object大小。