一文告诉你什么是内存对齐?
作者:@古明地盆
喜欢这篇文章的话,就点个关注吧,或者关注一下我的公众号也可以,会持续分享高质量Python文章,以及其它相关内容。:点击查看公众号
楔子
我们来解释一下什么是内存对齐,先来看个栗子:
#include <stdio.h>
typedef struct {
long a;
int b;
char c;
} S1;
typedef struct {
int b;
long a;
char c;
} S2;
int main() {
printf("%lu %lu\n", sizeof(S1), sizeof(S2)); // 16 24
}
两个结构体的成员是一样的,只是顺序不同,就造成结构体实例的大小不同,这就是内存对齐导致的。现代计算机的内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是 8 的倍数,这就是所谓的内存对齐。
32 位机器以 4 字节对齐,64 为机器以 8 字节对齐,我们后面就统一以 64 位机器举例。
为什么要进行内存对齐?
尽管内存是以字节为单位,但现在的 64 位处理器每次会以 8 字节为单位进行读取,假设没有内存对齐,那会发生什么呢?我们以上面的 S2 结构体为例。
因此没有内存对齐的话,在数据存储和读取的时候会做很多额外的工作。但有了内存对齐就不一样了,它保证了基础数据类型不会被读取两次。
内存对齐规则
内存对齐的规则很简单,首先按照 8 字节进行读取和存储,如果当前这个元素的字节数加上下一个元素的字节数超过了 8,那么该元素就要按照 8 字节进行对齐。我们实际操作一下:
#include <stdio.h>
typedef struct {
// a 占 4 个字节,下面的 b 占 1 字节,加一起没有超过 8,所以不用管
int a;
// a 加 b 总共 5 字节,而下面的 c 是 4 字节,加一起超过了 8 字节,所以内存对齐
// b 的下面会有 3 个字节的空洞
char b;
// 此时读取新的 8 字节,c 占 4 字节,d 占 1 字节,加起来没有超过 8 字节,所以不用管
float c;
// c + d 总共 5 字节,因此 8 字节块还剩下 3 字节,无法容纳下面是 8 字节的 e
// 因此内存对齐,d 下面也会多出 3 字节空洞
char d;
// 重新读取 8 字节,e 占 8 字节
long e;
// 重新读取 8 字节,f 占 4 字节,下面的 g 也是 4 字节,加一起没有超过 8 字节
float f;
// f + g 正好 8 字节,正好存下
int g;
// 重新读取 8 字节,h 占 1 字节,剩余的 7 字节无法容纳占 8 字节的 i
// 所以内存对齐,h 下面会有 7 字节的空洞
char h;
// 读取 8 字节,i 是 8 字节
long i;
// 读取 8 字节,k 是 4 字节,但由于 k 下面没有元素了,所以留下 4 字节的空洞
int k;
} S;
// 因此,我们计算一下总大小
/* a + b = 8 字节,其中 3 字节的空洞
* c + d = 8 字节,其中 3 字节的空洞
* e = 8 字节,正好容纳,相当于 0 字节的空洞
* f + g = 8 字节,正好容纳
* h = 8 字节,因为下面的 h + i 超过了 8 字节,所以 i 必须单独读取,于是会留下 7 字节的空洞
* i = 8 字节,正好容纳
* k = 8 字节,因为每次都是按照 8 字节读取,即使不够 8 字节,由于下面没有元素了,因此留下 4 字节的空洞
* 所以结构体 S1 实例总共占 56 个字节
*/
int main() {
printf("%lu\n", sizeof(S)); // 56
}
我们来画一张图,图像是个好东西:
然后我们再来分析最开始的栗子,为什么 S1 和 S2 的大小会不一样:
#include <stdio.h>
typedef struct {
// 读取 8 字节,存储 a
long a;
// 读取 8 字节,存储 b 和 c
int b;
char c;
} S1;
typedef struct {
// 读取 8 字节,存储 b,但剩余的 4 字节已无法存储 a,所以会有 4 字节的空洞
int b;
// 读取 8 字节,存储 a
long a;
// 读取 8 字节,存储 c
char c;
} S2;
int main() {
printf("%lu %lu\n", sizeof(S1), sizeof(S2)); // 16 24
}
所以这就是内存对齐导致的大小不一致,那么有没有办法改变内存对齐的方式呢?比如不按照 8 字节对齐,或者干脆紧密排列、不进行对齐。
#include <stdio.h>
#pragma pack(4) // 按照 4 字节对齐,可选值为 1、2、4、8、16
typedef struct {
// 读取 4 字节,此时没有空洞
int b;
// 读取 8 字节
long a;
// 读取 4 字节,存储 c
char c;
} S2;
int main() {
printf("%lu\n", sizeof(S2)); // 16
}
我们看到之前是 24 字节,现在变成了 16 字节,因为内存对齐的字节数被改变了。或者我们还可以禁止对齐:
#include <stdio.h>
#pragma pack(4)
typedef struct {
int b;
long a;
char c;
} S2;
typedef struct __attribute__ ((__packed__)) {
int b;
long a;
char c;
} S3;
int main() {
printf("%lu %lu\n", sizeof(S2), sizeof(S3)); // 16 13
}
我们看到禁止内存对齐之后,数据会紧密排列,此时只占 13 个字节。