1、内存对齐:
必要的基础知识:
- CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度
为什么要进行内存对齐?
- 平台(移植性)原因:==不是所有的硬件平台都能够访问任意地址上的任意数据。==例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况
- 性能原因:==若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算==。而本身就对齐的内存仅需要一次访问就可以完成读取动作
因此进行内存对齐的操作,实际上就是一种标准的**==“空间换时间”==**的做法。
默认对齐系数
在不同平台上的编译器都有自己默认的 “对齐系数”,一般来讲,我们常用的平台的系数如下:==32位:4 ;64位:8;==
对齐规则
- 结构体的成员变量,==第一个成员变量的偏移量为 0==。往后的每个成员变量的对齐值必须为编译器默认对齐长度(
#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。==其偏移量必须为对齐值的整数倍== - 结构体本身,对齐值必须为编译器默认对齐长度(
#pragma pack(n))或结构体的所有成员变量类型中的最大长度,取**==最大数的最小整数倍==**作为对齐值 - 结合以上两点,可得知若编译器默认对齐长度(
#pragma pack(n))超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的**==(默认对齐长度就是:2、4、6、8、16 、32等等)==**
代码示例:
type Part1 struct {
a bool
b int32
c int8
d int64
e byte
}
func main() {
part1 := Part1{}
fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}
//运行结果:part1 size: 32, align: 8
//Part1 内存布局:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx
详解:
成员对齐
- 第一个成员 a
- 类型为 bool
- 大小/对齐值为 1 字节
- 初始地址,偏移量为 0。占用了第 1 位
- 第二个成员 b
- 类型为 int32
- 大小/对齐值为 4 字节
- 根据规则 1,其偏移量必须为 4 的整数倍。确定偏移量为 4,因此 2-4 位为 Padding。而当前数值从第 5 位开始填充,到第 8 位。如下:axxx|bbbb
- 第三个成员 c
- 类型为 int8
- 大小/对齐值为 1 字节
- 根据规则 1,其偏移量必须为 1 的整数倍。当前偏移量为 8。不需要额外对齐,填充 1 个字节到第 9 位。如下:axxx|bbbb|c…
- 第四个成员 d
- 类型为 int64
- 大小/对齐值为 8 字节
- 根据规则 1,其偏移量必须为 8 的整数倍。确定偏移量为 16,因此 9-16 位为 Padding。而当前数值从第 17 位开始写入,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
- 第五个成员 e
- 类型为 byte
- 大小/对齐值为 1 字节
- 根据规则 1,其偏移量必须为 1 的整数倍。当前偏移量为 24。不需要额外对齐,填充 1 个字节到第 25 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e…
整体对齐
- 在每个成员变量进行对齐后,根据规则 2,整个结构体本身也要进行字节对齐,因为可发现它可能并不是
2^n,不是偶数倍。显然不符合对齐的规则。 - 根据规则 2,可得出对齐值为 8。现在的偏移量为 25,不是 8 的整倍数。因此确定偏移量为 32。对结构体进行对齐。
注意:
- 调整结构体中成员变量的顺序,有可能改变整个结构体占用的空间的大小,也就是减少Padding 所占用的空间。(调整结构体内成员变量的字段顺序就能达到缩小结构体占用大小的目的,是因为巧妙地减少了 Padding 的存在。让它们更 ==“紧凑”== 了。)
总结:
- 因为实际内存管理并非 “一个萝卜一个坑” 的思想。而是一块一块。通过**==空间换时间(效率)==**的思想来完成这块读取、写入。另外也需要兼顾不同平台的内存操作情况。