数据对齐方式

熟练掌握数据对齐和大小端,可能会在选择题考察,也会在大题中间接考察。

数据对齐

数据对齐(Data Alignment)是指数据在内存中的存放方式,它要求数据的起始地址必须是某个数(通常是1、2、4、8)的整数倍,这个数被称为对齐因子(Alignment Factor)。数据对齐的目的是为了提高内存访问的效率,因为许多计算机系统都是按照数据的对齐边界来设计内存访问硬件的。

不对齐的数据访问可能会导致性能下降,因为处理器可能需要额外的内存访问来获取不完整的数据。在一些严格要求数据对齐的架构中,不对齐的数据访问甚至会导致硬件异常。


在 C11 中,我们可以通过 _Alignof 来查看不同数据类型的对齐因子:

#include <stdint.h>
#include <stdio.h>

#define EVAL_PRINT(expr) printf("%-20s = %u\n", #expr, (uint8_t)(expr));

int main(void) {
    EVAL_PRINT(_Alignof(char));
    EVAL_PRINT(_Alignof(uint8_t));
    EVAL_PRINT(_Alignof(uint16_t));
    EVAL_PRINT(_Alignof(uint32_t));
    EVAL_PRINT(_Alignof(int));
    EVAL_PRINT(_Alignof(uint64_t));
    EVAL_PRINT(_Alignof(void*));
    EVAL_PRINT(_Alignof(size_t));
    return 0;
}

以上程序运行的结果如下所示,以此我们可以判断每个类型的 aglinment 大小。

$ gcc alignof.c && ./a.out
_Alignof(char)       = 1
_Alignof(uint8_t)    = 1
_Alignof(uint16_t)   = 2
_Alignof(uint32_t)   = 4
_Alignof(int)        = 4
_Alignof(uint64_t)   = 8
_Alignof(void*)      = 8
_Alignof(size_t)     = 8

以下图中的结构体定义为例,假设我们定义一个变量,变量的类型长度为 K 个字节,那么这个变量在内存中的地址 addr 必须是 K 的整数倍,即 addr % K == 0。

a
b
b
c
c
c
c
d
struct foo {
char a;
uint16_t b;
char d;
};
int32_t c;

上图中变量 b 和 a 之间增加了 1 个字节的 padding,变量 d 的末尾也增加了 3 个字节 padding,以保证下一个数据的开始是 4 的整数倍。

大小端

大小端(Endianness)是指多字节数据在内存中的字节序,也就是字节的排列顺序。主要有两种存放方式:

  • 大端模式(Big-Endian): 数据内部的高位字节存放在低位地址,低位字节存放在高位地址。也就是说,一个整数的第一个字节(最高有效字节)将存放在起始地址处。
  • 小端模式(Little-Endian): 数据内部的低位字节存放在低位地址,高位字节存放在高位地址。也就是说,一个整数的最后一个字节(最低有效字节)将存放在起始地址处。

举一个例子,假如定义数组 long a[2] = {0x76543210, 0xFEDCBA98}long 类型的大小为8字节,数组 a 在内存中的起始地址为 0x1000,则数组中两个元素在内存中的字节排列如下图所示:

0x10
0x32
0x54
0x65
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0x76
0x54
0x32
0x10
0x89
0xBA
0xDC
0xFE
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0x00
0xFE
0xDC
0xBA
0x89
0x1000
0x1000
0x1008
0x1008
Little Endian
Big Endian

大小端的选择通常是由计算机的CPU架构决定的,不同的架构有不同的字节序要求。例如,Intel x86和x86-64架构是小端,而网络协议通常是大端,因为大端的格式在字节流中的表示更加直观。