She*_*uan 16 c++ abi memory-alignment 32bit-64bit visual-c++
我想知道编译器是否会在32位和64位系统上使用不同的填充,因此我在一个简单的VS2019 C ++控制台项目中编写了以下代码:
struct Z
{
char s;
__int64 i;
};
int main()
{
std::cout << sizeof(Z) <<"\n";
}
Run Code Online (Sandbox Code Playgroud)
我对每个“平台”设置的期望:
x86: 12
X64: 16
Run Code Online (Sandbox Code Playgroud)
实际结果:
x86: 16
X64: 16
Run Code Online (Sandbox Code Playgroud)
由于x86上的存储字大小为4个字节,因此这意味着它必须以i
两个不同的字存储字节。所以我认为编译器将以这种方式进行填充:
struct Z
{
char s;
char _pad[3];
__int64 i;
};
Run Code Online (Sandbox Code Playgroud)
所以我可以知道背后的原因是什么?
Com*_*sMS 12
填充不是由字长决定的,而是由每种数据类型的对齐方式决定的。
在大多数情况下,对齐要求等于类型的大小。因此,对于像这样的64位类型,int64
您将获得8字节(64位)对齐。需要将填充插入结构中,以确保该类型的存储最终位于正确对齐的地址处。
当使用两种架构上具有不同大小的内置数据类型(例如指针类型(int*
))时,您可能会看到32位和64位之间的填充差异。
Pet*_*des 12
每个基元类型的大小和alignof()
(该类型的任何对象必须具有的最小对齐)是与架构的寄存器宽度分开的ABI 1设计选择。
结构打包规则也可能比将每个结构成员对齐到结构内的最小对齐更复杂;这是 ABI 的另一部分。
面向 32 位 x86 的 MSVC 给出__int64
的最小对齐为 4,但其默认结构打包规则将结构内的类型min(8, sizeof(T))
相对于结构的开头对齐。 (仅适用于非聚合类型)。这不是直接引用,这是我对@PW回答中的MSVC 文档链接的解释,基于 MSVC 似乎实际执行的操作。(我怀疑文本中的“以较少者为准”应该在括号之外,但也许他们对 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()
在变量或结构成员上指定 with来暗示类型. (例如,struct Z tmp
函数内堆栈上的局部变量将只有 4 字节对齐,因为 MSVC 不使用额外的指令,例如and esp, -8
将堆栈指针向下舍入到 8 字节边界。)
但是,new
/malloc
确实在 32 位模式下为您提供了 8 字节对齐的内存,因此这对于动态分配的对象(很常见)很有意义。强制堆栈上的局部变量完全对齐会增加对齐堆栈指针的成本,但通过设置结构布局以利用 8 字节对齐存储,我们可以获得静态和动态存储的优势。
这也可能是为了让 32 位和 64 位代码就共享内存的某些结构布局达成一致。(但请注意,x86-64 的默认值是min(16, sizeof(T))
,因此如果有任何 16 字节类型不是聚合(结构/联合/数组)并且没有alignas
. )
4 的最小绝对对齐来自 32 位代码可以假设的 4 字节堆栈对齐。 在静态存储中,编译器将为结构外的变量选择最多 8 或 16 字节的自然对齐,以便使用 SSE2 向量进行高效复制。
在较大的函数中,MSVC 可能会出于性能原因决定将堆栈对齐 8,例如,double
堆栈上的变量实际上可以用单个指令进行操作,或者也可能用于int64_t
SSE2 向量。请参阅2006 年这篇文章中的堆栈对齐部分:IPF、x86 和 x64 上的 Windows 数据对齐。因此,在 32 位代码中,您不能依赖于int64_t*
或double*
自然对齐。
(我不确定 MSVC 是否会自己创建更少对齐的int64_t
或double
对象。当然,如果您使用#pragma pack 1
or -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 位加载来加载/复制它,或 x87fild
进行 int64_t -> 双转换)。
这是 i386 System V ABI 的现代版本保持 16 字节堆栈对齐的原因之一:因此 8 字节和 16 字节对齐的本地变量是可能的。
在设计 32 位 Windows ABI 时,奔腾 CPU 至少已经出现。Pentium 有 64 位宽的数据总线,所以如果它是 64 位对齐的,它的 FPU 真的可以double
在单个缓存访问中加载64 位。
或者对于fild
/ fistp
,在转换为 / 时加载/存储一个 64 位整数double
。有趣的事实:在 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()
错误,直到它在 2018 年针对 g++8修复了一段时间,然后才针对 C11 修复_Alignof()
。请参阅该错误报告以根据标准中的引用进行一些讨论,该标准得出的结论是alignof(T)
应该真正报告您可以看到的最低保证对齐,而不是您想要的首选对齐性能。即使用int64_t*
小于alignof(int64_t)
对齐的 是未定义的行为。
(它通常在 x86 上可以正常工作,但是假设整个int64_t
迭代次数将达到 16 或 32 字节对齐边界的矢量化可能会出错。请参阅为什么未对齐访问 mmap'ed 内存有时会在 AMD64 上出现段错误?例如海湾合作委员会。)
gcc 错误报告讨论了 i386 System V ABI,它具有与 MSVC 不同的结构打包规则:基于最小对齐,不是首选。但是现代 i386 System V 保持 16 字节的堆栈对齐,因此编译器只在内部结构(因为结构打包规则是 ABI 的一部分)int64_t
和double
不太自然对齐的对象中创建。无论如何,这就是为什么 GCC 错误报告将结构成员作为特殊情况进行讨论的原因。
与带有 MSVC 的 32 位 Windows 正好相反,其中结构打包规则与alignof(int64_t) == 8
堆栈上的本地变量兼容,但堆栈上的本地变量始终可能未对齐,除非您使用alignas()
专门请求对齐。
32位MSVC具有奇异行为alignas(int64_t) int64_t tmp
是不一样的int64_t tmp;
,并且发射额外的指令以对齐堆叠。那是因为alignas(int64_t)
like alignas(8)
,它比实际最小值更对齐。
void extfunc(int64_t *);
void foo_align8(void) {
alignas(int64_t) int64_t tmp;
extfunc(&tmp);
}
Run Code Online (Sandbox Code Playgroud)
(32 位)x86 MSVC 19.20 -O2 像这样编译(在 Godbolt 上,还包括 32 位 GCC 和 struct 测试用例):
_tmp$ = -8 ; size = 8
void foo_align8(void) PROC ; foo_align8, COMDAT
push ebp
mov ebp, esp
and esp, -8 ; fffffff8H align the stack
sub esp, 8 ; and reserve 8 bytes
lea eax, DWORD PTR _tmp$[esp+8] ; get a pointer to those 8 bytes
push eax ; pass the pointer as an arg
call void extfunc(__int64 *) ; extfunc
add esp, 4
mov esp, ebp
pop ebp
ret 0
Run Code Online (Sandbox Code Playgroud)
但是没有alignas()
, 或alignas(4)
,我们会变得更简单
_tmp$ = -8 ; size = 8
void foo_noalign(void) PROC ; foo_noalign, COMDAT
sub esp, 8 ; reserve 8 bytes
lea eax, DWORD PTR _tmp$[esp+8] ; "calculate" a pointer to it
push eax ; pass the pointer as a function arg
call void extfunc(__int64 *) ; extfunc
add esp, 12 ; 0000000cH
ret 0
Run Code Online (Sandbox Code Playgroud)
它可以push esp
代替 LEA/push;这是一个小的错过的优化。
传递一个指向非内联函数的指针证明它不仅仅是局部地改变了规则。其他一些只将 anint64_t*
作为 arg 的函数必须处理这个可能未对齐的指针,而没有获得有关它来自何处的任何信息。
如果alignof(int64_t)
是真的8,该功能可手写的汇编的方式,在故障未对齐的指针。或者_mm_load_si128()
,在处理 0 或 1 个元素以达到对齐边界后,它可以用 C 编写,并使用 SSE2 内在函数,例如需要 16 字节对齐。
但是对于 MSVC 的实际行为,可能没有任何int64_t
数组元素按 16 对齐,因为它们都跨越 8 字节边界。
顺便说一句,我不建议__int64
直接使用特定于编译器的类型。您可以通过编写可移植代码int64_t
从<cstdint>
,又名<stdint.h>
。
在 MSVC 中,int64_t
将与__int64
.
在其他平台上,它通常是long
或long long
。 int64_t
保证正好是 64 位,没有填充和 2 的补码(如果有的话)。(它是针对普通 CPU 的所有健全的编译器。C99 和 C++ 要求long long
至少是 64 位,并且在具有 8 位字节和寄存器是 2 的幂的机器上,long long
通常正好是 64 位,可以用作int64_t
. 或者如果long
是 64 位类型,则<cstdint>
可以将其用作 typedef。)
我想__int64
和long long
在MSVC同一类型,但MSVC不严格执行走样反正所以它不会不管他们是否是完全相同的类型或没有,只是他们使用相同的表示。
这是对数据类型的对齐要求的问题,如结构成员的填充和对齐中所指定
每个数据对象都有一个对齐要求。除结构,并集和数组以外的所有数据的对齐要求是对象的大小或当前的包装大小(由
/Zp
或包编译指示指定,以较小者为准)。
并且在/ Zp(结构成员对齐)中指定了结构成员对齐的默认值。
下表描述了可用的包装值:
/自
Zp
变量效果
1在1字节边界上压缩结构。与/ Zp相同。
2在2字节边界上打包结构。
在4字节边界上打包4个结构。
8在8字节边界上打包结构(x86,ARM和ARM64的默认设置)。
16在16字节边界上打包结构(x64的默认设置)。
由于x86的默认值为/ Zp8,即8个字节,因此输出为16。
但是,您可以通过/Zp
选项指定其他包装尺寸。
这是一个实时演示,/Zp4
其输出为12而不是16。