0x00 面试题
下面是腾讯的面试题:
struct S
S
S.E
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
- 字长
N/8
2. 为什么要对齐
2, 4, 8
nn
数据结构应该尽可能地在自然边界上对齐。如果访问未对齐的内存,CPU需要做两次内存访问。
3. 数据结构对齐
看下go官方文档 Size and alignment guarantees 对于go数据类型的大小保证和对齐保证:
大小保证
在Go中,如果两个值的类型为同一种类的类型,并且它们的类型的种类不为接口、数组和结构体,则这两个值的尺寸总是相等的。
目前(Go 1.14),至少对于官方标准编译器来说,任何一个特定类型的所有值的尺寸都是相同的。所以我们也常说一个值的尺寸为此值的类型的尺寸。
下表列出了各种种类的类型的尺寸(对标准编译器1.14来说):
一个结构体类型的尺寸取决于它的各个字段的类型尺寸和这些字段的排列顺序。为了程序执行性能,编译器需要保证某些类型的值在内存中存放时必须满足特定的内存地址对齐要求。 地址对齐可能会造成相邻的两个字段之间在内存中被插入填充一些多余的字节。 所以,一个结构体类型的尺寸必定不小于(常常会大于)此结构体类型的各个字段的类型尺寸之和。
一个数组类型的尺寸取决于它的元素类型的尺寸和它的长度。它的尺寸为它的元素类型的尺寸和它的长度的乘积。
struct{}[0]T{}
对齐保证
go官方文档中的对对齐保证的要求只有如下解释:
xunsafe.Alignof(x)1xunsafe.Alignof(x)xunsafe.Alignof(x.f)1xunsafe.Alignof(x)
TNTNTN
事实上,每个类型有两个对齐保证。当它被用做结构体类型的字段类型时的对齐保证称为此类型的字段对齐保证,其它情形的对齐保证称为此类型的一般对齐保证。
Tunsafe.Alignof(t)tTunsafe.Alignof(x.t)TxtT
Ttreflect.TypeOf(t).Align()Treflect.TypeOf(t).FieldAlign()T
对于当前的官方Go编译器(1.14版本),一个类型的一般对齐保证和字段对齐保证总是相等的。
重排优化
看下下面的例子:
T1,T2int64
T1.a
T1.b
T1.c
8+8+8=24
T2cac
8+8=16
所以,合理重排字段可以减少填充,使 struct 字段排列更紧密
零大小字段对齐
zero sized fieldstruct{}final fieldfinal zero fieldfinal zero fieldfinal zero fieldfinal zero field
Estruct{}[0]int32[0]int64unsafe.Sizeof(S{})
64 位字安全访问保证
在 32 位系统上想要原子操作 64 位字(如 uint64)的话,需要由调用方保证其数据地址是 64 位对齐的,否则原子访问会有异常。
uint64uint64
来看下golang一个issue。
运行结果:
Cox大神的解答:
在32位系统上,开发者有义务使64位字长的数据的原子访问是64位(8字节)对齐的。在全局变量,结构体和切片的的第一个字长数据可以被认为是64位对齐的。
如何保证呢?
变量或开辟的结构体、数组和切片值中的第一个 64 位字可以被认为是 8 字节对齐
开辟的意思是通过声明,make,new 方式创建的,就是说这样创建的 64 位字可以保证是 64 位对齐的。
4. 总结
uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0mutex
0x03 拓展知识
依旧是开篇的例子,假设现在,连续声明了两个S的变量s1, s2,那s2的内存地址会是怎样的呢?
(0xc0000aa090-0xc0000aa060 = 48)
sizeclass
sizeclassspanpageobject
span8KBpage
spanobject
spanspanmspan
spanspanspanclassspanspan
mspanSize ClassobjectobjectobjectSize Classobjectmspanobjectobject