C++ 内存对齐

如下两个结构体 A B ,他们的实例的大小相同吗?

1
2
3
4
5
6
7
8
9
10
11
struct A{
char c_1;
int i;
char c2;
};

struct B{
char c_1;
char c_2;
int i;
};

两个 char 共占两个字节, 一个 int 占4个字节, 所以两个结构体都是 6个字节。但事实并非如此,在大部分计算机中,他们都不会只占 6 个字节。这涉及到 内存对齐
需知,计算机访问内存的方式,并不是一个字节一个字节访问的,而是以字长(word size)为单位来进行访问的。32位计算机的字长为 4 字节, 64位计算机的字节为 8 字节。而所谓内存对齐, 就是将变量调整至字长的倍数的位置存放,以方便计算机访问
上面两个结构体在内存中的布局最有可能是这个样子的:(这里说 最有可能 是因为具体的布局要视计算机而定)

我们使用一段代码来验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void showmem(unsigned char* p, uint32_t sz){
int len = sizeof(int*);
cout<<"\t";
for(int i = 0; i<len; i++){
cout<<i<<"\t";
}
cout<<endl;

for(int i = 0; i<=len; i++){
cout<<"-------|";
}
cout<<endl;

unsigned char* t = p;
uint32_t i = 0;
char msg[8];
while(i < sz){
sprintf(msg, "%02d", i);
cout<<msg<<"|\t";
for(int j = 0; j< len && (i+j)<sz; j++){
sprintf(msg, "%04d", *(uint8_t*)(t+j));
cout<<msg<<"\t";
}
cout<<endl;
i += len;
t += len;
}
}

////
//...
A a{'a', 2, 'b'};
B b{'a', 'b', 2};

cout<<"A:"<<endl;
showmem((unsigned char*)&a, sizeof(a));
cout<<endl;

cout<<"B:"<<endl;
showmem((unsigned char*)&b, sizeof(b));
cout<<endl;

结果如下:

1
2
3
4
5
6
7
8
9
10
A:
0 1 2 3 4 5 6 7
-------|-------|-------|-------|-------|-------|-------|-------|-------|
00| 0097 0000 0000 0000 0002 0000 0000 0000
08| 0098 0000 0000 0000

B:
0 1 2 3 4 5 6 7
-------|-------|-------|-------|-------|-------|-------|-------|-------|
00| 0097 0098 0000 0000 0002 0000 0000 0000

A 中,c_1 i 共占一个字,c_2 占一个字。这种将多个变量放入一个字中的动作,叫作 packing 。 而组成一个字时,补充不足的部分,叫 padding
如何知道某种类型的变量要对齐到哪个位置呢?它取决于一个类型的 alignment 如 int 类型的alignment 为4, 那么int 类型变量的地址必定为 4 的倍数。

alignof alignment

C++11 引入了 alignof 运算符。它类似于 sizeof 运算符, 返回一个类型的 alignment
我们来看一些数据类型的 alignment

1
2
3
4
5
6
7
8
9
#define SHOW_AL(T) \
cout<< #T << "\t --> sizeof: "<<sizeof(T)<<"\t"\
<<"alignof: "<<alignof(T)<<endl

///
SHOW_AL(char);
SHOW_AL(int);
SHOW_AL(A);
SHOW_AL(B);
1
2
3
4
char	 --> sizeof: 1	alignof: 1
int --> sizeof: 4 alignof: 4
A --> sizeof: 12 alignof: 4
B --> sizeof: 8 alignof: 4

对于内置类型, 其 alignment 就是其 size 。 对于复杂类型, 其 alignment 为其所有成员的 alignment 的最大值,其size 为完成对齐后的内存大小。如上,我们得知 A B 的alignment 均为4 (即 int 的 alignment 。 那么他们的实例的首地址一定是4的倍数。

alignas

以上我们看到的都是默认的alignment. 而很多语言允许用户控制alignment, 如 Rust, Assembly, 当然也包括 C/C++ .
C++11 提供了 alignas 关键字来完成这种控制。alignas 可以修饰 class struct union enumeration 或其非位域(non-bitfield)的成员 。 alignas(N) type 表示将 type 对齐到 N 字节。其中 N 必须为 2 的 n 次幂, 且不小于 alignof(type)
如下代码中,我们令 A 以 32 字节对齐, B::c_2 以 8 字节对齐。

1
2
3
4
5
6
7
8
9
10
11
struct alignas(32) A{
char c_1;
int i;
char c2;
};

struct B{
char c_1;
alignas(8) char c_2;
int i;
};

结果就是: A 将 padding 至 32 字节大小, 而 B.c_2 将对齐到第8字节上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A	 --> sizeof: 32	alignof: 32
B --> sizeof: 16 alignof: 8
A:
0 1 2 3 4 5 6 7
-------|-------|-------|-------|-------|-------|-------|-------|-------|
00| 0097 0000 0000 0000 0002 0000 0000 0000
08| 0098 0000 0000 0000 0000 0000 0000 0000
16| 0000 0000 0000 0000 0000 0000 0000 0000
24| 0000 0000 0000 0000 0000 0000 0000 0000

B:
0 1 2 3 4 5 6 7
-------|-------|-------|-------|-------|-------|-------|-------|-------|
00| 0097 0000 0000 0000 0000 0000 0000 0000
08| 0098 0000 0000 0000 0002 0000 0000 0000