每种原始类型的大小和alignof()
(该类型的任何对象必须具有的最小对齐方式)都是ABI1设计选择,与体系结构的寄存器宽度分开。
结构打包规则也可以比仅将每个结构成员与其在结构内的最小对齐方式对齐更为复杂;这是ABI的另一部分。
针对32位x86的MSVC将__int64
的最小对齐方式设置为4,但其默认的结构打包规则将结构中的类型与结构的起始位置对齐到min(8,sizeof(T))
(仅适用于非聚合类型)。这不是直接引用,而是我根据MSVC实际执行的MSVC文档链接从@P.W的回答中得出的意义。 (我怀疑文本中的“最小值”应该在括号外面,但也许他们正在就#pragma和命令行选项之间的交互提出不同的观点?)
(包含char [8]
的8字节结构在另一个结构内部仍然只能获得1字节的对齐方式,或者包含alignas(16)
成员的结构在另一个结构内部仍然获得16字节的对齐方式。)
请注意,ISO C++不保证原始类型具有
alignof(T) == sizeof(T)
。还要注意,MSVC对
alignof()
的定义与ISO C++标准不匹配:MSVC表示
alignof(__int64) == 8
,但某些
__int64
对象的对齐方式小于该值
2。
令人惊讶的是,即使MSVC不总是确保结构体本身具有超过4字节的对齐方式,我们仍然会获得额外的填充。除非您使用alignas()在变量或结构体成员上指定,以暗示该类型需要对齐(例如,函数内部堆栈上的局部struct Z tmp只有4字节的对齐方式,因为MSVC不使用额外的指令像and esp, -8这样将堆栈指针舍入到8字节边界)。
然而,在32位模式下,new/malloc确实会给您提供8字节对齐的内存,因此这对于动态分配的对象(这很常见)非常有意义。强制堆栈上的局部变量完全对齐将增加对齐堆栈指针的成本,但通过设置结构布局以利用8字节对齐的存储,我们可以获得静态和动态存储的优势。
这也可能是为了让32位和64位代码就共享内存的某些结构布局达成一致。 (但请注意,x86-64的默认值为
min(16,sizeof(T)),因此如果存在任何不是聚合体(struct / union / array)且没有alignas
的16字节类型,则它们仍然不能完全达成结构布局上的一致。)
最小绝对对齐度为4来自32位代码可以假设的4字节堆栈对齐。在静态存储中,编译器会选择自然对齐方式,但对于结构体外面的变量,为了与SSE2矢量进行高效复制,可能会选择最多8或16个字节的对齐方式。
在大型函数中,MSVC可能会决定通过将堆栈对齐为8来提高性能,例如对于堆栈上的double变量,实际上可以使用单个指令进行操作,或者也可能是使用SSE2向量处理int64_t。请参见此2006年文章中的“堆栈对齐”部分:Windows Data Alignment on IPF, x86, and x64。因此,在32位代码中,您不能依赖int64_t*或double*被自然对齐。
(我不确定MSVC是否会自己创建甚至更少对齐的int64_t或double对象。如果您使用#pragma pack 1或-Zp1,则肯定是这样,但这会改变ABI。但是否则可能不会,除非您手动从缓冲区中划出一个int64_t的空间并且不注意对齐。但是假设alignof(int64_t)仍为8,那将是C++未定义的行为。)
如果你使用 alignas(8) int64_t tmp
,MSVC会发出额外的指令来执行 and esp, -8
。 如果你不这样做,MSVC不会做任何特殊处理,所以 tmp
是否最终对齐到8字节取决于运气。
其他设计也是可能的,例如i386 System V ABI(用于大多数非Windows操作系统)具有alignof(long long)=4
但sizeof(long long)=8
。这些选择
除结构外(例如全局变量或堆栈上的本地变量),现代编译器在32位模式下确实选择将int64_t
对齐到8字节边界以提高效率(因此可以使用MMX或SSE2 64位加载或x87 fild
进行int64_t-> double转换)。
这就是为什么i386 System V ABI的现代版本保持16字节的堆栈对齐的一个原因:因此可以有8字节和16字节对齐的本地变量。
当32位Windows ABI被设计时,奔腾CPU至少已经在地平线上了。Pentium拥有64位宽的数据总线,因此如果它是64位对齐的,则其FPU确实可以在单个缓存访问中加载64位双精度。
或者对于fild / fistp,在将其转换为/自double时,加载/存储64位整数。有趣的是,x86上自然对齐的访问最多可保证原子性达到64位,自Pentium以来:为什么x86上自然对齐的变量整数赋值是原子的?
注1:ABI还包括调用约定,或在MS Windows的情况下,可以使用函数属性(例如__fastcall
)声明各种调用约定,但是原始类型(如long long
)的大小和对齐要求也是编译器必须协商一致的,以便创建可相互调用的函数。(ISO C++标准只涉及单个“C++实现”;ABI标准是“C ++实现”如何使自己与其他实现兼容的方式。)
请注意,结构体布局规则也是ABI的一部分:编译器必须就结构体布局达成一致,以创建可相互传递结构体或指向结构体的指针的兼容二进制文件。否则,s.x = 10; foo(&x);
可能会写入与预期不同的偏移量相对于结构体的基址,而分别编译的foo()
(可能在DLL中)希望在该偏移量处读取。
注释2:
GCC也有这个C++ alignof()的bug,直到2018年g++8修复之后才被修复,这是在修复C11 _Alignof()之后的一段时间。请参见该错误报告以了解基于标准的一些讨论,其中得出结论:alignof(T)应该报告您可以看到的最小保证对齐方式,而不是为性能而希望的首选对齐方式。即使用具有小于alignof(int64_t)对齐方式的int64_t*是未定义行为。
(在x86上通常可以正常工作,但假设整数次数的向量化将达到16或32字节对齐边界的矢量化可能会失败。请参见使用gcc的为什么对mmap'ed内存进行非对齐访问有时会在AMD64上导致segfault?作为示例。)
gcc 的错误报告讨论了 i386 System V ABI,其具有与 MSVC 不同的结构填充规则:基于最小对齐而非首选。但现代 i386 System V 保持 16 字节堆栈对齐,所以只有在结构体内部(因为结构填充规则是 ABI 的一部分)编译器才会创建小于自然对齐的 int64_t 和 double 对象。无论如何,这就是 GCC 错误报告讨论结构成员作为特殊情况的原因。
与 MSVC 的 32 位 Windows 相反,struct-packing 规则与 alignof(int64_t) == 8 兼容,但堆栈上的局部变量始终可能不符合对齐要求,除非您使用 alignas() 明确请求对齐。
32 位 MSVC 具有奇怪的行为,即 alignas(int64_t) int64_t tmp 与 int64_t tmp; 不同,并发出额外的指令来对齐堆栈。这是因为 alignas(int64_t) 就像 alignas(8),比实际最小值更对齐。
void extfunc(int64_t *);
void foo_align8(void) {
alignas(int64_t) int64_t tmp;
extfunc(&tmp);
}
(32位) x86 MSVC 19.20 -O2编译它时像这样(在Godbolt上,还包括32位GCC和结构体测试用例):
_tmp$ = -8
void foo_align8(void) PROC
push ebp
mov ebp, esp
and esp, -8
sub esp, 8
lea eax, DWORD PTR _tmp$[esp+8]
push eax
call void extfunc(__int64 *)
add esp, 4
mov esp, ebp
pop ebp
ret 0
但是没有使用alignas()
,或者使用alignas(4)
,我们得到了更简单的结果
_tmp$ = -8
void foo_noalign(void) PROC
sub esp, 8
lea eax, DWORD PTR _tmp$[esp+8]
push eax
call void extfunc(__int64 *)
add esp, 12
ret 0
它可以只使用push esp
而不是LEA/push; 这是一个小的未优化。
传递指向非内联函数的指针证明它不仅仅是在本地弯曲规则。一些其他函数只会得到int64_t*
作为参数,必须处理这个可能未对齐的指针,而没有获得任何关于它来自哪里的信息。
如果alignof(int64_t)
真的是8,那么该函数可以用汇编手写方式编写,以一种在错误对齐指针时导致故障的方式。或者它可以使用SSE2内置函数(如_mm_load_si128()
)用C语言编写,要求16字节对齐,之前需要处理0或1个元素以达到对齐边界。
但是根据MSVC的实际行为,可能没有一个int64_t
数组元素被16字节对齐,因为它们全部跨越8字节边界。
顺便提一下,我不建议直接使用编译器特定类型如__int64
。你可以使用<cstdint>
中的int64_t
编写可移植代码。
在MSVC中,int64_t
将与__int64
相同。
在其他平台上,它通常是long
或long long
。 int64_t
保证精确地为64位,没有填充,并且是2的补码形式(如果提供)。(所有针对普通CPU的理智编译器都是这样。C99和C++要求long long
至少为64位,在具有8位字节和寄存器的机器上,long long
通常恰好为64位,可以用作int64_t
。或者,如果long
是64位类型,则<cstdint>
可能会将其用作typedef。)
我假设__int64
和long long
在MSVC中是相同的类型,但是MSVC不强制执行严格别名,因此它们是否完全相同并不重要,只需使用相同的表示即可。
int64_t
的默认对齐方式是8字节?,32位Intel处理器上的内存对齐。 - Daniel Langr