一文告诉你什么是内存对齐?

楔子

我们来解释一下什么是内存对齐,先来看个栗子:

#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 个字节。

posted @ 2019-10-18 16:43  古明地盆  阅读(1372)  评论(1编辑  收藏  举报