注:每天进步一点点,记录每日成长,思考的第09/365天 ;
本文预计阅读时间: 7分钟
1
1
01
为什么要对齐
对齐原因:
1)平台原因(移植性原因):
不是所有的硬件平台都能够访问任意地址上的任意数据的。例如,某些平台只允许在某些特定地址处获取特定类型的数据,否则会抛出异常。
2)性能原因:
实际上CPU并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是一块一块读取的。块大小(2、4、6、8、16等字节大小)我们称其为内存访问粒度。
如果访问未对齐的内存,将导致cpu进行两次内存访问,并且需要花费额外的时间来处理对齐和运算。
1
02
默认系数
在不同平台上的编译器都有自己默认的 “对齐系数”,可通过预编译命令 #pragma pack(n)
进行变更,n 就是代指 “对齐系数”。一般来讲,我们常用的平台的系数如下:
32 位:4
64 位:8
使用函数Alignof()、Offsetof()
返回相应类型的对齐系数、偏移量:
| 功能 | 函数 |
|---|---|
| 获取对齐值 | unsafe.Alignof() |
| 获取偏移值 | unsafe.Offsetof() |
func main() {
fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
fmt.Printf("slice align: %d\n", unsafe.Alignof([]int{1,2,3 }))
fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY"))
fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{}))
}
//对齐值分别为:
bool align: 1
int32 align: 4
int8 align: 1
int64 align: 8
byte align: 1
slice align: 8
string align: 8
map align: 8
1
03
对齐规则
1)结构体的成员变量, 第一个成员变量的偏移量是0. 往后的每个成员变量的对齐值必须为编译器默认长度或当前成员变量类型的长度(unsafe.Sizeof()), 取最小值作为当前类型的对齐值。其偏移量必须为当前成员变量的对齐值的整数倍;
2)结构体本身, 对齐值必须为编译器默认的对齐长度或结构体所有的成员变量类型中的最大长度, 取最大数的最小整数倍作为对齐值。
(注:最大数指的是所有对齐长度中的最大值,或者所有成员变量长度中的最大值)
type stu struct {
a bool
b int8
c int32
d int64
e byte
g []int
f string
}
func main() {
stu1 := stu{}
fmt.Printf("stu1 size: %d, align:%d\n",unsafe.Sizeof(stu1), unsafe.Alignof(stu1))
//stu1 size: 64, align:8
}
使用的64位CPU
,对齐参数是8来分析,bool
、int8
、int32
、int64
、byte、[]int32
、string
对齐值分别是 1、1、4、8、1、8
、8
,占用内存大小分别是1、1、4、8、1、24
、16
,我们先根据第一条对齐规则分析stu1
:
第一个字段类型是 bool
,对齐值是 1,大小为 1,偏移量为 0 ;第二个字段类型是 int8
,对齐值是 1,大小为1 ,根据规则一,所以他的内存偏移值必须是1
的倍数,前面不需填充 ;第三个字段类型是 int32,对齐值是 4,大小为 4,所以他的内存偏移值必须是 4 的倍数,且前两个字段排到 第2
位,第3,4
位由编译器进行填充,一般为0
值,也称之为空洞。第 5 位到第8
位为第三个字段 c ;第四个字段类型是 int64
,对齐值是 8,大小为 8,所以他的内存偏移值必须是 8 的倍数,因为stu1
前三个字段就已经排到了第8
位,所以下一位的偏移量正好是 8,正好是字段 d 的对齐值的倍数,不用填充,可以直接排列第四个字段,也就是从第9
位到16
位第三个字段 d ;第五个字段类型是 bool
,对齐值是 1,大小为 1,根据规则一,直接排第 17 位 ;第六个字段类型是 []int
,对齐值是8
,大小为24
,所以他的内存偏移值必须是 8 的倍数,因为stu1
前面字段就已经排到了第17
位,我们目前的内存长度是17
,不是字段e
的对齐值的倍数,所以从第18
位到 24 位需要填充。第24
位到 48 位第六个字段 e.;第七个字段类型是 string
,对齐值是8
,大小为16
,所以他的内存偏移值必须是 8 的倍数,所以下一位的偏移量正好是48
。正好是字段 f 的对齐值的倍数,不用填充。好了现在第一条内存对齐规则后,内存长度已经为 64 字节,我们开始使用内存的第二条规则进行对齐。根据第二条规则,默认对齐值是 8
,字段中最大类型程度是24
,取最小的那一个,所以求出结构体的对齐值是8
,我们目前的内存长度是 64,是8
的倍数,所以不再需要补齐。
1
04
特别注意
空 struct{}
大小为 0,作为其他 struct 的字段时,一般不需要内存对齐。但有一种特许情况:即当 struct{}
作为结构体最后一个字段时,需要内存对齐。
1
1
05
总结
1)内存对齐是为了cpu更高效访问内存数据;
2)结构体对齐依赖成员变量的大小保证和对齐保证;
3)地址对齐保证是:如果类型t的对齐保证是n,那么类型t的每个值的地址在运行时必须是n的倍数;
4)struct内字段如果填充过度,可以尝试重排,使字段排序更紧密,减少内存浪费;
5)零大小字段要避免作为struct最后一个字段。




