结构体内存对齐
推荐学习:
前言知识
- 字
word:是用于表示其自然的数据单位,也叫machine word。字是电脑用来一次性处理事务的一个固定长度。 - 字长:计算机进行一次整数运算所能处理的二进制数据的位数 字的长度
- 字节
byte:1字节=8位(1byte= 8 bit) - 位
bit
什么是内存对齐
CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问,提高内存访问效率。比如 64 位的 CPU ,字长为 8 字节,那么 CPU 访问内存的单位也是 8 字节。
为保证程序顺利高效运行,编译器会把各种数据类型安排到合适的地址并占用合适长度,确保CPU访问内存次数减少,这就是内存对齐。
为什么需要内存对齐
CPU 始终以字长访问内存,如果不进行内存对齐,很可能增加 CPU 访问内存的次数,影响性能。
以32位CPU举例:
每次读取会按照字长作为单位去读取内存数据。
如果非内存对齐,那么在读取 b 数据时候,就需要分两次读,第一次读取地址 0-3 取 3 (一个字节)第二次读取地址 4-7 取 4-5(两个字节),在进行拼接。
如果内存对齐,那么无论读取 a 还是 b 数据,都是可以通过一次读取字长来获得。
总结:合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。
对齐系数
每种类型的对齐值就是他的对齐系数,内存对齐要求数据存储地址以及占用字节数是内存对齐的倍数。
怎么确定每个类型的对齐值呢?
这个与平台上的编译器有关。每个特定平台上的编译器都有自己的默认"对齐系数",常用平台默认对齐系数如下:
- 32位系统对齐系数是4
- 64位系统对齐系数是8
而数据类型的对齐系数是取类型大小与平台的对齐系数种较小的那个,注意同一个类型在不同平台上的对齐系数不同。
unsafe 标准库提供了 Alignof 方法,可以返回一个类型的对齐值。
func main() {
var x string
fmt.Println(unsafe.Alignof(x)) //8
}
为什么不统一使用平台最大对齐值呢或者就采用各个类型对应的对齐值?
举例:当前平台是 64 位的,最大对齐系数为 8byte
存储类型为 int8 占 1 字节
假设要在 32 位平台,最大对齐系数为 4byte
存储类型为 int64 占 8 字节
目的都是减少浪费,提高性能。
注意:你要明白对齐系数到底是什么?他是一种帮助变量根据自身的存储容量来更好选择存储地址的一个值。
他只是来帮助你在一串内存中更好的选择存储地址,存储变量,减少内存访问次数。
所有的行为与选择都是为了减少内存浪费和提高性能的。
结构体内存对齐规则
怎么确定一个结构体的对齐系数
func main() {
type T struct {
a int8 //1
b int64 //8
c int32 //4
d int16 //2
}
fmt.Println(unsafe.Alignof(T{}))//8
fmt.Println(unsafe.Sizeof(T{}))//24
}
先确定结构体中每一个成员的值,选择最大的作为结构体的对齐系数。
看看结构体如何存储
首先,结构体的起始地址需要是对齐系数的倍数。结构体的每一个字段都要把起始地址当作地址0,根据对齐系数决定自己放在哪儿。
接下来,逐个分析每个字段:
- a 是第一个字段,他的对齐系数为 1,所以从第 0 个位置开始占据 1 个字节
- b 是第二个字段,他的对齐系数为 8,所以不能从位置 1 开始,开始位置需要时 8 的倍数,所以从第 8 个位置开始占据 8 个字节
- c 是第三个字段,他的对齐系数为 4,接下来的位置(索引为 16)满足 4 的倍数,所以第 16 个位置开始占据 4 个字节
- d 是第四个字段,他的对齐系数为 2,第 20 个位置满足 2 的倍数,所以第 20 个位置开始占据 2 个字节
所有成员都放好了,其实还没有结束。
第二个要求:占据的字节满足结构体对齐系数的倍数
如果到第四个字段结束,它只占据了 22 个字节,不满足倍数,还需要扩展 2 个字节,以满足 8 的倍数。
最终这个结构体类型大小是 24 字节
如果改变字段的顺序,他的类型大小会发生改变吗?
答:会的!
type demo1 struct {
a int8
b int16
c int32
}
type demo2 struct {
a int8
c int32
b int16
}
func main() {
fmt.Println(unsafe.Sizeof(demo1{})) // 8
fmt.Println(unsafe.Sizeof(demo2{})) // 12
}
至于为什么你可以通过方法去分析理由!
空的结构体会怎么对齐?
空的结构体大小为 0,作为其他的结构体字段时候,会不需要内存对齐。但是有一种情况除外:当空的结构体为其他结构体的最后一个字段时候,会需要内存对齐。因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)。
type demo3 struct {
c int32
a struct{}
}
type demo4 struct {
a struct{}
c int32
}
func main() {
fmt.Println(unsafe.Sizeof(demo3{})) // 8
fmt.Println(unsafe.Sizeof(demo4{})) // 4
}
我猜测他的内存对齐与上一个字段一致。