数据对齐 - 2023.2 简体中文

Vitis 高层次综合用户指南 (UG1399)

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 简体中文

软件程序员习惯于将存储器视为简单的字节阵列,而将基本数据类型视为是由一个或多个存储器块组成的。但计算机处理器并不会在单个字节大小的区块内对存储器执行读取和写入。现今的现代化 CPU 实际上每次访问 2、4、8、16 甚至是 32 字节的区块,虽说最常用的指令集架构 (ISA) 是 32 位和 64 位的。鉴于系统内存储器的组织方式,这些区块的地址应为其大小的倍数。如果地址满足此要求,则将其称为已对齐。高级程序员对于存储器的看法与现代化处理器实际处理存储器的方式之间的差别在应用程序的正确性和性能方面实际上是非常重要的。例如,如果您不了解软件中的地址对齐问题,那么就可能发生下列情况:

  • 软件运行缓慢
  • 应用将锁定/挂起
  • 操作系统可能崩溃
  • 软件将静默失败,产生错误结果

C++ 语言可以提供一系列不同大小的基本类型。为了能快速操作这些类型的变量,生成的对象代码将尝试使用能立即读取/写入整个数据类型的 CPU 指令。这也就意味着这些类型的变量在存储器内的布局方式应确保其地址能以合适方式保持对齐。由此导致每个基本类型除大小之外还有另一个属性:即对齐要求。基本类型的对齐可能看似与其大小相同。但实际情况往往并非如此,因为最适合特定类型的 CPU 指令可能每次只能访问其数据中的一部分。例如,32 位 x86 GNU/Linux 机器可能每次只能读取最多 4 字节,因此,64 位 long long 类型的大小为 8,对齐为 4。下表显示了 C/C++ 中对应 32 位和 64 位 x86-64 GNU/Linux 机器的基本原生数据类型的大小和对齐(以字节数为单位)。

表 1. 数据类型
类型 32 位 x86 GNU/Linux 64 位 x86 GNU/Linux
大小 对齐 大小 对齐
bool 1 1 1 1
char 1 1 1 1
short int 2 2 2 2
int 4 4 4 4
long int 4 4 8 8
long long int 8 4 8 8
float 4 4 4 4
double 8 4 8 8
long double 12 4 16 16
void* 4 4 8 8
根据以上排列,程序员为什么需要更改对齐?原因有多个,但主要原因是在存储器要求与性能之间的取舍。在主机与加速器之间往返发送数据时,发射的每个字节都有成本。幸好,GCC C/C++ 编译器可提供语言扩展 __attribute__ ((aligned(X))),用于为变量、结构/类或结构字段更改默认对齐(以字节数为单位来测量)。例如,以下声明会导致编译器在 16 字节边界上分配全局变量 x。
int x __attribute__ ((aligned (16))) = 0;

__attribute__((aligned (X))) 不会更改所应用到的变量的大小,而是可以更改结构的存储器布局,方法是在结构体的各元素之间插入填充。由此即可更改结构的大小。如果您在已对齐的属性中不指定对齐因子,那么编译器会将声明的变量或字段的对齐设置为要编译的目标机器上的任意数据类型所使用的最大对齐。这样通常能够提升复制操作的效率,因为编译器在对已通过这种方式对齐的变量或字段执行往来复制时,可以使用任何能复制最大存储器区块的指令。aligned 属性只能增大对齐,而不能减小对齐。C++ 函数 offsetof 可用于判定结构中每个成员元素的对齐。