1️⃣ 什么是内存对齐

定义

内存对齐(Memory Alignment) 通常指的是:

  • 数据在内存中的起始地址要满足某种“倍数关系”。

比如一个 int 通常是 4 字节,如果它要求 4 字节对齐,那么它的起始地址往往需要是 4 的倍数

double 常见要求 8 字节对齐,则起始地址需要是 8 的倍数。

1
2
3
4
5
6
7
8
9
10
11
//32位系统
#include<stdio.h>
struct {
int x;
char y;
}s;

int main() {
printf("%d\n",sizeof(s));
return 0;
}

此时的输出结果是 8B,而不是想象中的 5B = 4B + 1B。

原因

  • 现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始
  • 但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数 k(通常它为4 或 8)的倍数,这就是所谓的内存对齐。

C/C++ 里,对齐主要体现在两个地方

  • 单个对象的对齐要求
    • 每种类型都有一个对齐值,记作 alignof(T),表示 T 的对象地址至少要按多少字节对齐。
  • 结构体/类的布局
    • 编译器会在成员之间插入一些“看不见的字节”(padding,填充字节),让每个成员都满足自己的对齐要求。
    • 同时还会让整个对象的大小 sizeof 变成对齐值的整数倍,方便数组存放。

2️⃣ 为什么要内存对齐

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的

它一般会以双字节,四字节,8 字节,16 字节甚至 32 字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度

在考虑 4 字节存取粒度的处理器取 int 类型变量(32位系统),该处理器只能从地址为 4 的倍数的内存开始读取数据。

重点案例

假如没有内存对齐机制,数据可以任意存放,现在一个 int 变量存放在从地址 1 开始的四字节地址中,该处理器去取数据时,要先从 0 地址开始读取第一个 4 字节块,剔除不想要的字节(0 地址),然后从地址 4 开始读取下一个 4 字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器,这需要做很多工作。

现在有了内存对齐的,int 类型数据只能存放在按照对齐规则的内存中,比如说 0 地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。

3️⃣ 内存对齐规则

每个特定平台上的编译器都有自己的默认的对齐系数

  • gcc 中默认 #pragma pack(4),可以通过预编译命令 #pragma pack(n),n = 1,2,4,8,16来改变这一系数。

有效对齐值

指的是给定值 #pragma pack(n) 和结构体中最长数据类型长度中较小的那个。

内存对齐规则

  • 成员对齐:结构体内每个成员的起始偏移必须是该成员对齐值的倍数
  • 整体对齐:结构体的整体对齐值是其成员对齐值的最大值(或受 #pragma pack 等限制)。
  • 尾部填充:结构体最终大小必须是整体对齐值的整数倍(为了数组中每个元素都对齐)。

案例

此时不考虑 #pragma pack 的情况下,sizeof (A) 的大小应该为 24B;

1
2
3
4
5
struct A {
int x; // 4 字节,对齐 4
double y; // 8 字节,对齐 8
char z; // 1 字节,对齐 1
};
  • x 放在偏移 0,占 4B,即 0 - 3B。
  • y 需要 8 对齐,但当前偏移是 4,不满足,编译器在 3B 插入 4B padding,让 y 从偏移 8B 开始。
  • z 放在 y 后面
  • 为了让整体大小是 8 的倍数,末尾再补齐 padding

如果考虑 #pragma pack(4) 的情况下,sizeof (A) 的大小应该为 16B;

1
2
3
4
5
struct A {
int x; // 4 字节,对齐 4
double y; // 8 字节,被压到对齐 4
char z; // 1 字节,对齐 1
};
  • x:0~3
  • y:从 4 开始(因为只要求 4 对齐),占 8 字节 → 4~11
  • z:12
  • 末尾补齐到 4 的倍数:再补 3 字节