对于提供的答案,似乎对对齐实际上是什么感到有些困惑。混淆可能是因为有两种对齐方式。
1. 会员对齐
这是一种定性度量,它说明对于结构/类类型中成员的特定排序,实例的字节数有多大。一般来说,如果成员在结构中按字节大小降序排列(即最大的在前,最小的成员在后),编译器可以压缩结构/类实例。考虑:
struct A
{
char c; float f; short s;
};
struct B
{
float f; short s; char c;
};
两种结构都包含完全相同的信息。为了这个例子;float 类型占用 4 个字节,short 类型占用 2 个字节,字符占用 1 个字节。但是,第一个结构 A 具有随机顺序的成员,而第二个结构 B 根据其字节大小对成员进行排序(这在某些架构上可能会有所不同,我假设 x86 intel CPU 架构在此示例中具有 4 字节对齐)。现在考虑结构的大小:
printf("size of A: %d", sizeof (A)); // size of A: 12;
printf("size of B: %d", sizeof (B)); // size of B: 8;
如果您希望大小为 7 个字节,您会假设成员使用 1 字节对齐方式打包到结构中。虽然一些编译器允许这样做,但由于历史原因(大多数 CPU 使用 DWORD(双字)或 QWORD(四字)通用寄存器),通常大多数编译器使用 4 字节甚至 8 字节对齐。
有 2 种填充机制在工作以实现打包。
首先,如果结果字节大小小于或等于字节对齐,则每个字节大小小于字节对齐的成员将与下一个成员“合并”。在结构B中,成员s和c可以这样合并;它们的组合大小是 s 的 2 个字节 + c 的 1 个字节 == 3 个字节 <= 4 字节对齐。对于结构 A,不会发生这种合并,并且每个成员在结构的打包中有效地消耗了 4 个字节。
再次填充结构的总大小,以便下一个结构可以从对齐边界开始。在示例 B 中,总字节数为 7。下一个 4 字节边界位于字节 8,因此该结构用 1 个字节填充,以允许数组分配作为实例的紧密序列。
请注意,Visual C++ / GCC 允许 1 个字节、2 个和 2 个字节的更高倍数的不同对齐方式。了解这不利于您的编译器为您的架构生成最佳代码的能力。实际上,在以下示例中,每个字节都将作为单个字节读取,每次读取操作都使用一条单字节指令。实际上,硬件仍然会获取包含读取到缓存中的每个字节的整个内存行,并执行该指令 4 次,即使这 4 个字节位于同一个 DWORD 中并且可以在 1 条指令中加载到 CPU 寄存器中。
#pragma pack(push,1)
struct Bad
{
char a,b,c,d;
};
#pragma pack(pop)
2.分配对齐
这与上一节中解释的第二种填充机制密切相关,但是,分配对齐可以在malloc() / memalloc()分配函数的变体中指定,例如std::aligned_alloc()。因此,与结构/对象类型的字节对齐所建议的对齐边界不同(通常为 2 的更高倍数)对齐边界分配对象是可能的。
size_t blockAlignment = 4*1024; // 4K page block alignment
void* block = std::aligned_alloc(blockAlignment, sizeof(T) * count);
该代码会将类型 T 的计数实例块放置在以 4096 的倍数结尾的地址上。
使用这种分配对齐的原因再次纯粹是架构性的。例如,从页面对齐地址读取和写入块更快,因为地址范围非常适合缓存层。跨越不同“页面”的范围会在跨越页面边界时破坏缓存。不同的媒体(总线架构)具有不同的访问模式,并且可能受益于不同的对齐方式。通常,4、16、32 和 64 K 页面大小的对齐并不少见。
请注意,语言版本和平台通常会提供这种对齐分配功能的特定变体。例如,Unix/Linux 兼容的posix_memalign()函数通过 ptr 参数返回内存,并在失败的情况下返回非零错误值。
- int posix_memalign(void **mempr, size_t 对齐, size_t 大小); // POSIX(Linux/UX)
- void *aligned_alloc(size_t 对齐,size_t 大小); // C++11
- void *std::aligned_alloc(size_t 对齐,size_t 大小); // c++17
- void *aligned_malloc(size_t 大小, size_t 对齐); 微软VS2019