几种编程语言之结构体内存对齐
写在前面
计算机的内存空间是以 byte 来划分,因此我们可以简单的认为不同的数据类型,就占有相应大小的内存空间,但实际此举会为内存的访问带来效率上的问题。实际内存访问是会以一个固定字长来开始访问,而这样就会产生一个问题,有些数据类型的字长会大于或小于这个固定字长。
为了解决这个问题,不同的编译器会对不同的数据类型做一些规定,会分两步走:一个是每个数据类型所占的空间,应该是某个字节数的整数倍;另一个是如果实际类型变量所占的空间达不到规定的大小,就会做填充,使其符合。这个过程就是内存对齐。
内容
可以从 Go 语言里看一个例子:
package main
import (
"fmt"
"unsafe"
)
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
}
可以看到,两个结构体里的属性其实是一样的,只是它们的顺序有所不同,导致打印出来的 size 也不同。那为什么呢?
以 demo1 为例,第一个变量类型为 int8,所占为 1 个字节,编译器规定为 1 的倍数,而它也是第一个,从下标为0的偏移量开始,所以无需对齐。
第二个变量 int16,规定对齐倍数是2,故需要在下标偏移量为2的地方开始,并且占2个字节,它前面的偏移量1会留空填充。
第三个变量 int32,规定对齐倍数是4,故需要在下标偏移量为4的地方开始,并且占4个字节。
因此 demo1 所占的空间为 1 + 1 + 2 + 4 = 8 个字节。
而 demo2 里,同样第一个变量类型为 int8,所占为 1 个字节,编译器规定为 1 的倍数,而它也是第一个,从下标为0的偏移量开始,所以无需对齐。
第二个变量 int32,规定对齐倍数是4,故需要在下标偏移量为4的地方开始,并且占4个字节。故前面的1、2、3会作为留空填充。
第三个变量 int16,规定对齐倍数是2,故需要在下标偏移量为8的地方开始,并且占2个字节。
因此似乎 demo2 所占的空间为 1 + 3 + 4 + 2 = 10 个字节。
这里就需要引入另外一个东西 unsafe.Alignof:
package main
import (
"fmt"
"unsafe"
)
type demo1 struct {
a int8
b int16
c int32
}
type demo2 struct {
a int8
c int32
b int16
}
func main() {
fmt.Println(unsafe.Alignof(demo1{})) // 4
fmt.Println(unsafe.Alignof(demo2{})) // 4
}
这个东西叫对齐保证,因为这里我们要处理的是 demo1、demo2 这一整个的特定类型,通过对齐保证,我们可以得到一个数字,这个数字告诉我们,这个特定类型实际所占的空间,将会是这个数字的整数倍。
由于demo1 的对齐保证是4,刚好里面的各个属性的空间+对齐空间为8字节,为4的整数倍。
而 demo2 的对齐保证也是4,但里面各个属性的空间+对齐空间为10字节,不是4的整数倍,故最后一个 int16 类型的变量,需要再填充对齐2个字节,使得实际是12个字节,为4的整数倍。
在C语言里也有类似的例子:
#include <stdio.h>
typedef struct t1{
char x;
int y;
double z;
}T1;
typedef struct t2{
char x;
double z;
int y;
}T2;
int main(int argc, char* argv[])
{
printf("sizeof(T1) = %lu\n", sizeof(T1)); // 16
printf("sizeof(T2) = %lu\n", sizeof(T2)); // 24
return 0;
}
有时候如果我们对一些内存空间要求比较高的地方,可以通过分析内存对齐,调整字段的位置,来作为一种优化的手段。但大多数时候应该是没必要去关注这些的吧。
参考
C语言内存对齐详解
Go语言101-内存布局
Go struct 内存对齐
Size and alignment guarantees