一、内存对齐
1.对齐的目的
性能优化
CPU 访问内存时是按总线宽度(比如 4 字节、8 字节)取数据的。
如果数据自然对齐(比如
int
按 4 字节边界放置),一次内存访问就能取到完整数据。如果数据未对齐(比如
int
从奇地址开始),CPU 可能要:读取两次内存
再把结果组合起来
→ 导致速度下降,有些架构甚至需要额外指令来处理。
类比:就像搬箱子时,如果箱子正好对齐在卡车门口,一次就能搬完;如果卡在两个门之间,你得搬两次再拼起来。
硬件访问限制
有些 CPU(尤其是老 ARM、MIPS、DSP)不允许非对齐访问,一旦不对齐会直接触发硬件异常(Bus Error)。
例如:
在 ARM Cortex-M3 上读取未对齐的
uint32_t
可能直接 HardFault。在 x86 上虽然可以非对齐访问,但会有性能损耗。
举个例子:
假设 CPU 总线一次能取 4 字节:
地址: 0x100 0x101 0x102 0x103 | 0x104 0x105 0x106 0x107
[ word 1 ] | [ word 2 ]
对齐访问:
int
从0x100
取,一次取完。未对齐访问:
int
从0x102
取,要:取
0x100~0x103
取
0x104~0x107
拼出目标数据。
这就是为什么对齐能快,而且有些硬件甚至不允许不对齐。
2.malloc返回值为什么要对齐
看RT-Thread源码时注意到的,之前没了解过
malloc 分配的内存会被用来存放任意类型的变量,比如:
int *p1 = malloc(100); // int 可能需要 4 字节对齐
double *p2 = malloc(100);// double 可能需要 8 或 16 字节对齐
如果 malloc 返回的地址不是按最大类型的对齐要求来分配的,那么当你把它转成需要更高对齐的指针时,就可能:
性能下降(CPU 要做两次访问)
在部分 CPU 上直接 硬件错误.
所以,malloc 必须保证返回的地址满足目标平台上最大对齐类型的要求(例如 8 或 16 字节)。
下面是一个结构体对齐 + malloc 内部布局的综合示意图,把 malloc 的头部、padding、用户数据全画出来了,来理解对齐 + 内存分配的完整过程。
场景
平台对齐要求:8 字节
**malloc 内部有一个头部(header)**存储块大小等信息,占 6 字节(只是示例,真实实现可能不同)
用户申请:10 字节数据
不对齐的错误示例
原始内存(假设 malloc 从 0x1000 开始分配):
0x1000 [malloc header 6B] [用户数据 10B]
^ ^ 返回地址
| └── 0x1006 <-- 不是 8 字节倍数
对 double/int64 来说,这个地址不满足 8 字节对齐要求:
- CPU 要分两次取数(性能差)
- 某些 CPU 上直接硬件错误
malloc 对齐后的正确示例
原始内存块:
0x1000 [malloc header 6B][padding 2B][用户数据 10B]
^ header ^ pad ^ 返回地址 (0x1008)
说明:
- header 占 6 字节
- malloc 自动加 2 字节 padding,使返回地址是 8 的倍数
- 返回地址 0x1008 可以安全存放任何类型
地址: 0x1000 0x1004 0x1008 0x100C 0x1010
|header(4B)|hdr(2B) | pad(2B) |用户数据...
↑ malloc 内部管理信息
↑ 对齐用的 padding
↑ malloc 返回值(保证对齐)
malloc 返回值对齐的目的:
安全性:避免某些架构上访问未对齐数据导致崩溃(Bus Error)。
性能:让 CPU 一次取数据,不用拆成多次访问。
通用性:malloc 返回的内存可以存放任何类型(包括最大对齐要求的类型)。
那malloc是怎么实现对齐的呢
RT-Thread中的rt_malloc使用
aligned_addr = (addr + align - 1) & ~(align - 1);
来实现对齐,其中align是要对齐的字节数。这个应该是在malloc内部已经实现了,不需要自己实现。
3.除了malloc需要对齐,还有其他比较经典的情况需要对齐吗?
硬件寄存器映射(外设驱动)
场景:访问 MCU 或 SoC 的外设寄存器(MMIO,Memory-Mapped I/O)。
寄存器通常是 32 位对齐(4 字节),甚至 64 位对齐。
如果结构体映射寄存器时没有对齐,会导致访问异常(总线错误)。
示例:
typedef struct {
uint32_t CTRL; // 0x00
uint32_t STATUS; // 0x04
uint32_t DATA; // 0x08
} __attribute__((aligned(4))) UART_RegDef;
原因:硬件总线一次取 4B,不对齐访问可能直接硬 Fault(尤其 ARM Cortex-M)。
DMA 缓冲区
场景:DMA(直接内存访问)传输要求源/目的地址对齐到总线宽度(常见 4、8、16 字节)。
如果不对齐,DMA 控制器可能拒绝启动,或者自动截断数据。
示例:
uint8_t __attribute__((aligned(16))) dma_buffer[256];
原因:DMA 硬件一次搬一整个对齐宽度的块,不对齐会浪费带宽甚至出错。
缓冲区优化(cache line 对齐)
场景:多线程共享缓存行时会产生伪共享(false sharing),可以用缓存行(常见 64 字节)对齐来优化性能。
示例:
struct Counter {
int value;
} __attribute__((aligned(64)));
原因:避免两个线程修改同一缓存行,减少缓存同步开销。
文件系统 / 存储对齐
场景:底层 NAND/NOR Flash、SD 卡、SSD 块设备,通常要求访问按扇区对齐(512B、4KB)。
示例:
uint8_t __attribute__((aligned(512))) sector_buf[512];
原因:底层控制器按扇区读写,非对齐操作会引发额外的读-改-写。
特定协议包(网络对齐)
场景:网络协议头(TCP/UDP/IP)有时会对齐到 4 字节或 8 字节边界,方便 CPU 快速解析。
示例:
struct ip_header {
uint8_t ver_ihl;
uint8_t tos;
uint16_t total_length;
} __attribute__((packed, aligned(4)));
原因:网络包解析时能直接用 32 位读取,不需要额外拼接。
4.Linux内核中为什么同时使用了不少packed, aligned
控制结构体内存布局,保证精确字节格式(尤其是协议/硬件映射)
内核经常需要和硬件寄存器、网络协议包格式、磁盘文件系统结构等二进制格式严格对应。
这些数据格式要求字段紧凑排列,不允许有多余的填充字节,此时用
packed
保证紧凑无缝隙。但是,硬件或平台对整体数据对齐有要求,比如寄存器地址要对齐,或者DMA传输数据块对齐。
这时候用
aligned(n)
强制整个结构体或数据块的起始地址满足对齐要求,避免性能或硬件异常。
避免单纯 packed
带来的整体性能问题
只用
packed
结构体,虽然节省空间,但CPU访问非对齐成员可能非常慢。用
packed, aligned(n)
,可以在保证内部紧凑的前提下,保证结构体起始地址对齐,有利于整体访问性能(比如读写结构体整体的指针操作、DMA传输等)。虽然结构体成员仍然可能非对齐,但通常内核会通过特殊代码或者逐字段访问避免性能瓶颈。
不同层面需求的折中
packed
:保证成员之间无间隙,适合协议和硬件映射。aligned
:保证整体地址对齐,避免整体访问异常和提升DMA效率。内核里有很多不同场景:
有些地方需要结构体紧凑,但访问方式是“逐字节拷贝”,不用CPU按成员直接访问。
有些地方结构体内成员会经过专门处理,不是直接读写内存中的非对齐成员。
有些场景下对齐到16字节、32字节是硬件需求,比如SIMD、缓存行对齐等。
访问效率分析
同时使用可能对访问结构体整体有提高,但对访问其中的成员还是会降低效率。
访问整个结构体时有利于缓存和SIMD,但这对单个成员访问影响有限。
结构体本身起始地址8字节对齐,访问整个结构体时有利于缓存和SIMD,但这对单个成员访问影响有限。
内部成员仍然是非对齐访问,因为
packed
取消了内部对齐,b
偏移1字节,不是4字节对齐。CPU对非对齐的int访问依然可能需要多周期或者触发异常处理,性能下降。