liangbm3's blog

Back

1. 内存对齐简介#

在讨论内存对齐之前,我们先来看一下计算机底层是怎么对数据进行存取的,这涉及到计算机组成原理的知识。

这主要涉及计算机中的三大总线:地址总线、数据总线和控制总线。地址总线决定数据的地址,数据总线里面传输的是数据,控制总线传输的是命令和状态信号。数据总线的宽度直接决定了一个CPU周期可以同时传送多少位。CPU的设计是高度协同的,寄存器和数据总线的宽度一般相同(Intel早期的8086的寄存器宽度为16位,总线宽度为8位),我们平时所说的xx位的计算机实际上指的是CPU寄存器的宽度,例如:

  • 32位计算机:CPU寄存器和数据总线的宽度都为32位
  • 64位计算机:CPU寄存器和数据总线的宽度都为64位

现代计算机中,内存空间的最小单位是字节,因为CPU的地址总线以字节为最小寻址粒度。但是,大部分的处理器并不是按字节来存取内存的,一般会以2字节,4字节,8字节来存取内存,我们称之为内存存取粒度。这个存取的宽度就取决于我们上面介绍的总线宽度,例如32位计算机每次传输的是32位(4字节),而64位计算机每次传输的是64位(8字节)。

以32位计算机为例,假设我们要在0x0001这个地址中写入一个字符'A'(1字节)。CPU会将0X0000放在地址总线,让内存控制器知道目标在0x00000x0003那个内存块,然后会将字符'A'放在数据总线的对应位置,即4字节块中的第二个字节,CPU会使能第二天字节使能线,这个控制指令放在控制总线中,这样CPU可以在对应的位置写入而不影响内存块中的其他字节。

再举个例子,假设在一个32位的计算机中,要读取一个地址为0x1001int变量,那么这个4字节的int变量占用的内存是0x1001 - 0x1005。由于32位计算机的内存存取粒度是4,因此该处理器只能从地址为4的倍数的内存开始读取数据。第一次会读取0x1000 - 0x1003,剔除不需要的字节(0x1000),然后第二次读取0x1004 - 0x1007,剔除不要的数据(0x10050x10060x1007),最后将数据合并完成读取。

内存对齐是什么呢?其实内存对齐是一种优化技术,旨在提高CPU访问内存的速度,从以上的例子可以看出,虽然int的大小等于内存存取粒度,但是由于存放的位置,CPU需要进行两次读取。而内存对齐则是对数据再内存中存放的位置的限制,通常会要求数据的首地址的值是某个数k的倍数。例如如果规定int的首地址只能是4的倍数,那么对int的读取就可以一次性完成。

2. 内存对齐规则#

一些核心概念如下:

  • 自身对齐值:这是数据类型本身固有的对齐要求,通常等于其 sizeof 的大小。
    • char: 1 字节
    • short: 2 字节
    • int: 4 字节
    • float: 4 字节
    • double: 8 字节
    • long: 4 字节(32为系统)或 8 字节(64位系统)
    • long long: 8 字节
    • 指针: 4 字节(32位系统)或 8 字节(64位系统)
  • 指定对齐值:这是程序员通过编译器指令(如 C/C++ 中的 #pragma pack(n)) 手动设定的对齐值。n 的值通常是1, 2, 4, 8等2的幂。
  • 有效对齐值 (Effective Alignment):这是编译器最终为某个数据成员采用的对齐值。它的计算规则是: 有效对齐值 = Min(自身对齐值, 指定对齐值)。

所有的数据结构,无论是struct还是union,在布局时都遵循以下三条规则:

  1. 结构体中每个成员相对于结构体起始地址的偏移量,必须是其有效对齐值的整数倍。如果不是,编译器会在前一个成员后面填充若干字节以满足要求。
  2. 结构体(或联合体)的总大小,必须是其所有成员中最大的有效对齐值的整数倍。如果不是,编译器会在最后一个成员后面填充若干字节以满足要求。
  3. 如果一个结构体包含了另一个嵌套结构体,那么这个嵌套结构体的起始偏移量,必须是其内部所有成员中最大的有效对齐值的整数倍。

3. 内存对齐举例#

在64位系统中,一个结构体定义如下:

struct MyStruct {
    char a;         // 自身对齐1
    int b;          // 自身对齐4
    double c;       // 自身对齐8
    short d;        // 自身对齐2
};
cpp
  • 第一个成员a,有效对齐值为1,当前偏移量为0,是1的倍数,因此当前大小为1字节。
  • 第二个成员b,有效对齐值4,当前偏移量为1,不是4的倍数,前面要填充3字节,因此当前大小为8字节。
  • 第三个成员c,有效对齐值为8,当前偏移量为8,是8的倍数,因此当前大小为16字节。
  • 第四个成员d,有效对齐值为2,当前偏移量为10,是2的倍数,因此当前大小为18字节。
  • 在结构体中,最大有效对齐值为8,18不是8的倍数,需要填充到24,后面填充6个字节。

因此这个结构体在内存中的布局如下:

alt text

下面举一个指定对齐值的例子:

#pragma pack(push, 2) // 设置指定对齐值为2
struct MyStructPacked {
    char a;         // 自身对齐1->有效对齐min(1,2)=1
    int b;          // 自身对齐4->有效对齐min(4,2)=2
    double c;       // 自身对齐8->有效对齐min(8,2)=2
    short d;        // 自身对齐2->有效对齐min(2,2)=2
};
#pragma pack(pop) // 恢复默认
cpp
  • 第一个成员a,有效对齐值为1,当前偏移量为0,是1的倍数,因此当前大小为1字节。
  • 第二个成员b,有效对齐值为2,当前偏移量为1,不是2的倍数,前面要填充1字节,因此当前大小为6字节。
  • 第三个成员c,有效对齐值为2,当前偏移量为6,是2的倍数,因此当前大小为14字节。
  • 第四个成员d,有效对齐值为2,当前偏移量为14,是2的倍数,因此当前大小为16字节。
  • 在结构体中,最大有效对齐值为2,16是8的倍数,不需要再填充,因此总大小为16字节。

因此这个结构体在内存中的布局如下:

alt text

C++ 中的内存对齐
https://liangbm3.site/blog/c-zhong-de-nei-cun-dui-qi
Author liangbm3
Published at 2025年6月28日
Comment seems to stuck. Try to refresh?✨