C C++ 正确理解字节对齐

字节对齐定义

字节对齐是计算机编程中,数据在内存中的起始地址必须是其自身大小的整数倍的一种内存布局规则。

可以把内存想象成一个一个连续的、大小为1字节(Byte)的小格子,每个格子都有一个唯一的地址,从0开始编号:

地址: 0 1 2 3 4 5 6 7

  • 一个 char 类型占 1 字节,可以放在任何地址上(地址是 1 的倍数)。
  • 一个 short 类型占 2 字节,必须放在地址为 0, 2, 4, … 这样的位置(地址是 2 的倍数)。
  • 一个 int 类型占 4 字节,必须放在地址为 0, 4, 8, … 这样的位置(地址是 4 的倍数)。
  • 一个 long long 类型占 8 字节,必须放在地址为 0, 8, 16, … 这样的位置(地址是 8 的倍数)。

如果数据的起始地址不符合这个规则,就称为未对齐。

为什么需要字节对齐?

为什么计算机要搞这么一套规则呢?主要原因有以下两个

性能优化(最主要原因)

计算机的 CPU 读取内存时,并不是一个字节一个字节地读,而是按块(Block)读取的。这个块的大小通常是 CPU 的字长(Word Size),比如 32 位 CPU 的块大小是 4 字节,64 位 CPU 的块大小是 8 字节。

对齐的情况

假设我们要读取一个 4 字节的int,它的地址是 4。CPU 一次就能从地址 4 开始,把 4、5、6、7 这一个完整的块读入寄存器,效率很高。

未对齐的情况

假设这个int的地址是 5。CPU 需要先读取地址 4-7 这个块,然后再读取地址 8-11 这个块,之后还要从这两个块中把 5-8 这 4 个字节的数据提取出来并组合在一起,才能得到完整的int值。这显然需要更多的 CPU 周期,性能更低。

硬件限制

某些硬件平台(特别是一些嵌入式系统或比较老的 CPU)不支持访问未对齐的内存地址。如果强行访问,会导致硬件异常(Hardware Exception)或直接使程序崩溃。即使是现代的 x86/x86_64 架构 CPU 支持未对齐访问,也会有明显的性能损失。

字节对齐规则

基本数据类型的自身对齐值

对于 C/C++ 中的基本数据类型,其自身对齐值通常等于其sizeof的大小,基础类型内存对齐值如下:

  • char:sizeof=1,对齐值 = 1
  • short:sizeof=2,对齐值 = 2
  • int:sizeof=4,对齐值 = 4
  • long:sizeof=4(32位系统)或 8(64位系统),对齐值 = 4 或 8
  • long long:sizeof=8,对齐值 = 8
  • float:sizeof=4,对齐值 = 4
  • double:sizeof=8,对齐值 = 8
  • 指针:sizeof=4(32位系统)或 8(64位系统),对齐值 = 4 或 8

结构体的整体对齐规则

结构体的总大小必须是其内部所有成员中最大对齐值的整数倍这是为了保证当该结构体作为另一个结构体的成员时,也能满足对齐要求。

结构体成员的对齐规则

结构体中的每个成员,其存放的起始地址必须是该成员自身对齐值的整数倍。如果前一个成员的结束地址无法满足当前成员的对齐要求,编译器会在中间填充(Padding)一些字节来调整地址。

示例一

struct Example
{
char a; // 1字节
short b; // 2字节
int c; // 4字节
};

  • char a 放在地址 0。
  • short b 的对齐值是 2,所以它的起始地址必须是 2 的倍数。前一个成员a在地址 0,占用1字节,下一个可用地址是 1地址1不是 2 的倍数,所以编译器会在a和b之间填充1个字节(Padding 1 字节),然后将b放在地址 2。
  • int c 的对齐值是 4,所以它的起始地址必须是 4 的倍数前一个成员b在地址 2,占用 2 字节,结束于地址 3下一个可用地址是 4,正好是 4 的倍数,所以c直接放在地址 4。
  • 结构体成员总占用字节:1 (a) + 1 (Padding) + 2 (b) + 4 (c) = 8 字节。
  • 结构体的最大对齐值是int的 4,因为8是4的整数倍,所以此示例中结构体的总大小就是8字节。

示例二

struct Example
{
char a; // 1字节
int c; // 4字节
short b; // 2字节
};

  • char a 放在地址 0。
  • int c 的对齐值是 4,所以它的起始地址必须是 4 的倍数。需要在a后面填充 3 个字节,然后将c放在地址 4。
  • short b 的对齐值是 2。前一个成员c在地址 4,占用 4 字节,结束于地址 7。下一个可用地址是 8,是 2 的倍数,所以b放在地址 8。
  • 结构体成员总占用字节:1 (a) + 3 (Padding) + 4 (c) + 2 (b) = 10 字节。
  • 结构体的最大对齐值是int的 410不是4的整数倍,需要在末尾填充2个字节,因此本示例中结构体的总大小为12字节(12 是 4 的整数倍)。

对比示例一和示例二可以发现:结构体成员的声明顺序会影响结构体的总大小为了节省内存,通常建议将占用空间小的成员放在前面,大的放在后面,但也要综合考虑可读性。

如何控制字节对齐

在某些情况下比如需要和硬件设备或网络协议的特定内存布局打交道,或者为了极致地节省内存,我们可能需要手动改变编译器的默认对齐规则。

在 C/C++ 中,可以使用编译器指令 #pragma pack 来实现,用法如下:

  • #pragma pack(n):将当前的对齐值设置为 nn必须是2的幂次方(1, 2, 4, 8, 16 等)。
  • #pragma pack():恢复到默认的对齐方式。

示例

#pragma pack(1) // 设置对齐值为1,即按1字节对齐
struct PackedStruct
{
char a;
int b;
short c;
};
#pragma pack() // 恢复默认对齐

// 在1字节对齐下,结构体成员之间没有任何填充
// 总大小 = 1 + 4 + 2 = 7字节

#pragma pack 是一个非常强大的工具,但也是一把双刃剑。使用它可能会破坏代码的可移植性,并可能引入性能问题,因此在使用前必须深思熟虑。

总结

字节对齐的目的是为了提升 CPU访问内存的性能和满足某些硬件平台的要求而设计的内存布局规则。

  • 核心思想:数据的起始地址必须是其自身大小(或指定对齐值)的整数倍。
  • 结构体对齐:成员按自身对齐值排列,中间可能有填充结构体总大小是最大成员对齐值的整数倍。
  • 手动控制:可以使用 #pragma pack 等编译器指令改变默认对齐行为,但需谨慎使用。

本文介绍了内存对齐的概念以及内存对齐的规则,正确的理解字节对齐规则对于编写高效、可移植的底层代码(如嵌入式开发、驱动开发以及网络通信消息封装)至关重要

 

首发于:https://mp.weixin.qq.com/s/sZ88sXL_8XZypSgVdjy7RA?token=83664354&lang=zh_CN

发表在 C/C++ | 留下评论