什么是内存对齐
数据在内存中并不是按连续存放的,为了更快的读取内存中的数据,编译器把程序中的每个数据单元安排在适当的位置上。CPU从内存中读取数据也不是一个字节一个字节的去读取,CPU是按照一定的粒度从内存中读取数据的。内存对齐就是通过空间换时间(效率)来加速内存的读取、写入。
一、unsafe包中的三个函数
在介绍内存对齐之前首先要了解unsafe包中的三个重要函数,函数签名如下:
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
1.1 unsafe.Sizeof
这个函数可以返回一个类型所占用的内存大小,这个大小只有类型有关,和类型对应的变量存储的内容大小无关。
代码示例:
func TestUnsafeSizeof(t *testing.T) {
t.Log(unsafe.Sizeof(true)) //1
t.Log(unsafe.Sizeof(int8(0))) //1
t.Log(unsafe.Sizeof(int16(0))) //2
t.Log(unsafe.Sizeof(int32(0))) //4
t.Log(unsafe.Sizeof(int64(0))) //8
}
1.2 unsafe.Alignof
unsafe.Alignof返回一个类型的对齐系数,是结构体中单位基本类型所占的内存数,如果元素是数组那么取数组元素类型所占的内存,与编译器默认的对齐系数(一般为8)取较小的那个进行内存对齐,也就是说Alignof最大返回8。结构体实际大小为对齐系数的整数倍。
下面代码可以看到这个结构体对齐系数为8,一共占用24个字节,在这里字段a、b、c一起占用8个字节,字段d占用16个字节。
func TestAlignof(t *testing.T) {
type test struct {
a int8
b int16
c int32
d [2]int64
}
tt := test{}
t.Log(unsafe.Alignof(tt.a)) //1
t.Log(unsafe.Alignof(tt.b)) //2
t.Log(unsafe.Alignof(tt.c)) //4
t.Log(unsafe.Alignof(tt.d)) //8
t.Log(unsafe.Alignof(tt)) //8
t.Log(unsafe.Sizeof(tt)) //24
}
1.3 unsafe.Offsetof
unsafe.Offsetof只适用于结构体中的字段相对于结构体的内存位置偏移量。结构体的第一个字段的偏移量都是0。
通过这个函数可以直接根据结构体字段的偏移量对其进行操作,哪怕是未导出的字段同样可以使用该函数进行读写操作。(需要配合uintptr进行指针运算)
下面是通过结构体内存偏移量对字段进行操作的示例:
func TestOffsetof(t *testing.T) {
type Boy struct {
age int8
weight int32
name string
}
boy := Boy{}
ptr := unsafe.Pointer(&boy)
agePtr := unsafe.Pointer(uintptr(ptr) + unsafe.Offsetof(boy.age))
weightPtr := unsafe.Pointer(uintptr(ptr) + unsafe.Offsetof(boy.weight))
namePtr := unsafe.Pointer(uintptr(ptr) + unsafe.Offsetof(boy.name))
*(*int8)(agePtr) = 24
*(*int32)(weightPtr) = 68
*(*string)(namePtr) = "小张"
t.Log(boy) // {24 68 小张}
}
二、内存对齐
2.1 内存对齐的原因
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
例如如果把一个int64的数据写到的0x0001开始,而不是0x0000开始,那么数据并没有存在同一行列地址上。因此cpu必须得让内存工作两次才能取到完整的数据。效率自然就很低。
2.2 内存对齐的验证
由下面的例子我们可以看出,各个字段的内存大小总和是小于结构体所占用的内存的,这是因为编译器在字段中间加上填充字节(internal padding)。
func TestMemoryAlign1(t *testing.T) {
type Eg struct {
a byte
b int16
c int64
d int32
}
eg := Eg{}
sa := unsafe.Sizeof(eg.a)
sb := unsafe.Sizeof(eg.b)
sc := unsafe.Sizeof(eg.c)
sd := unsafe.Sizeof(eg.d)
sizeCount := sa +sb + sc +sd
sizeEg := unsafe.Sizeof(eg)
t.Log(sizeCount) //15
t.Log(sizeEg) //24
}
2.3 结构体内存布局的分析
在下面代码中我们通过unsafe.Offsetof配合unsafe.Alignof来分析上面结构体在内存中的布局,可以看到这个结构体的对齐系数是8。然后根据字段的偏移量我们可以推断出这个结构体在内存中的布局为:a0bb0000|cccccccc|dddd0000(这里使用"0"来代表padding)
func TestMemoryAlign2(t *testing.T) {
type Eg struct {
a byte
b int16
c int64
d int32
}
eg := Eg{}
t.Log(unsafe.Alignof(eg)) //8
t.Log(unsafe.Offsetof(eg.a)) //0
t.Log(unsafe.Offsetof(eg.b)) //2
t.Log(unsafe.Offsetof(eg.c)) //8
t.Log(unsafe.Offsetof(eg.d)) //16
}
2.4 结构体字段顺序对内存占用的影响
下面我们把结构体字段换一种排列方式,可以看出这时结构体所占内存大小变为了16字节!然后推测出这时的内存布局为 a0bbdddd|cccccccc(同样使用"0"来代表padding)
func TestMemoryAlign3(t *testing.T) {
type Eg struct {
a byte
b int16
d int32
c int64
}
eg := Eg{}
t.Log(unsafe.Alignof(eg)) //8
t.Log(unsafe.Sizeof(eg)) //16
t.Log(unsafe.Offsetof(eg.a)) //0
t.Log(unsafe.Offsetof(eg.b)) //2
t.Log(unsafe.Offsetof(eg.c)) //8
t.Log(unsafe.Offsetof(eg.d)) //4
}
三、总结
通过对结构体中字段顺序的合理安排可以减少padding的存在,使内存分配更加的紧凑,避免内存的浪费。我们可以借助unsafe包提供的函数,可以分析出结构体在内存中的布局从而找到合适的字段顺序,达到内存优化的作用。