我想知道编译器是否会在32位和64位系统上使用不同的填充,所以我在一个简单的VS2019 C ++控制台项目中编写了下面的代码:
struct Z
{
char s;
__int64 i;
};
int main()
{
std::cout << sizeof(Z) <<"\n";
}
我对每个“平台”设置的期望:
x86: 12
X64: 16
实际结果:
x86: 16
X64: 16
由于x86上的存储器字大小是4个字节,这意味着它必须将i
的字节存储在两个不同的字中。所以我认为编译器会以这种方式填充:
struct Z
{
char s;
char _pad[3];
__int64 i;
};
那么我可以知道这背后的原因是什么?
每种基本类型的大小和alignof()
(该类型的任何对象必须具有的最小对齐)是与架构的寄存器宽度分开的ABI1设计选择。
结构包装规则也可能比仅将每个结构成员对齐到结构内部的最小对齐更复杂;这是ABI的另一部分。
针对32位x86的MSVC为__int64
提供了最小对齐4,但其默认的struct-packing规则将结构中的类型与min(8, sizeof(T))
相对于结构的开头对齐。 (仅适用于非聚合类型)。这不是一个直接的引用,这是我对来自@ P.W答案的MSVC docs link的解释,基于MSVC实际上做的事情。 (我怀疑文本中的“以较小者为准”应该是在parens之外,但也许他们对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字节边界。)
但是,new
/ malloc
确实在32位模式下为您提供了8字节对齐的内存,因此这对于动态分配的对象(这是常见的)非常有意义。强制堆栈上的本地对象完全对齐会增加对齐堆栈指针的成本,但通过设置struct layout以利用8字节对齐的存储,我们可以获得静态和动态存储的优势。
这也可能旨在获得32位和64位代码,以便就共享内存的某些结构布局达成一致。 (但请注意,x86-64的默认值是min(16, sizeof(T))
,因此如果有任何16字节类型不是聚合(struct / union / array)并且没有,那么它们仍然不完全同意struct layout alignas
。)
4的最小绝对对齐来自32位代码可以假设的4字节堆栈对齐。在静态存储中,编译器将为结构外部的变量选择自然对齐,最多可能为8或16个字节,以便使用SSE2向量进行有效复制。
在较大的功能中,出于性能原因,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 - >双转换)。
这就是现代版i386 System V ABI保持16字节堆栈对齐的原因之一:因此可以实现8字节和16字节对齐的本地变量。
当设计32位Windows ABI时,奔腾CPU至少还在眼前。 Pentium具有64位宽的数据总线,因此如果它的64位对齐,它的FPU实际上可以在单个高速缓存访问中加载64位double
。
或者对于fild
/ fistp
,在转换为/从double
转换时加载/存储64位整数。有趣的事实:自从奔腾:Why is integer assignment on a naturally aligned variable atomic on x86?以来,自然对齐的访问最多64位在x86上保证原子
脚注1:ABI还包括一个调用约定,或者在MS Windows的情况下,可以选择各种调用约定,你可以用__fastcall
等函数属性来声明,但是像long long
这样的基本类型的大小和对齐要求也是编译器必须同意制作可以相互调用的函数。 (ISO C ++标准仅涉及单个“C ++实现”; ABI标准是“C ++实现”如何使它们彼此兼容。)
请注意,struct-layout规则也是ABI的一部分:编译器必须在struct layout上相互一致,以创建传递结构或指向结构的指针的兼容二进制文件。否则s.x = 10; foo(&x);
可能写入相对于结构基础的不同偏移量而不是单独编译的foo()
(可能在DLL中)期望读取它。
脚注2:
海湾合作委员会也有这个C ++ alignof()
错误,直到它被修复为C11 fixed in 2018 for g++8一段时间后才是_Alignof()
。根据标准中的引用查看该错误报告,该标准得出结论:alignof(T)
应该真正报告您可以看到的最小保证对齐,而不是您想要的性能首选对齐。即使用低于int64_t*
对齐的alignof(int64_t)
是不确定的行为。
(它通常在x86上工作正常,但是假设整个int64_t
迭代将达到16或32字节对齐边界的向量化可能会出错。请参阅Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?以获取gcc的示例。)
gcc bug报告讨论了i386 System V ABI,它具有与MSVC不同的结构包装规则:基于最小对齐,不是首选。但是现代的i386 System V保持了16字节的堆栈对齐,所以它只是在结构内部(因为结构包装规则是ABI的一部分),编译器创建的int64_t
和double
对象不是自然对齐的。无论如何,这就是GCC错误报告讨论结构成员作为特例的原因。
与具有MSVC的32位Windows相反,其中struct-packing规则与alignof(int64_t) == 8
兼容,但堆栈上的locals总是可能未完全对齐,除非您使用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像这样编译它(on 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
但是如果没有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
它可能只是push esp
而不是LEA /推;这是次要的错过优化。
将指针传递给非内联函数证明它不仅仅是局部弯曲规则。一些其他函数只是得到一个int64_t*
作为arg必须处理这个潜在的欠对齐指针,而没有得到任何关于它来自何处的信息。
如果alignof(int64_t)
真的是8,那么这个函数可以用asm中的手写方式写入错误的指针。或者可以用C语言编写SSE2内在函数,如_mm_load_si128()
,需要16字节对齐,处理0或1个元素后才能到达对齐边界。
但是对于MSVC的实际行为,有可能没有一个int64_t
数组元素被16对齐,因为它们都跨越了8字节的边界。
顺便说一句,我不建议直接使用像__int64
这样的编译器特定类型。您可以使用int64_t
from <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无论如何都不强制执行严格别名,因此它们是否完全相同并不重要,只是它们使用相同的表示。
填充不是由字大小决定的,而是由每种数据类型的the alignment决定的。
在大多数情况下,对齐要求等于类型的大小。因此,对于像int64
这样的64位类型,您将获得8字节(64位)对齐。需要将填充插入到结构中以确保该类型的存储最终位于正确对齐的地址。
当使用在两种体系结构上具有不同大小的内置数据类型(例如指针类型(int*
))时,您可能会看到32位和64位之间填充的差异。
这是Padding and Alignment of Structure Members中指定的数据类型的对齐要求问题
每个数据对象都有一个对齐要求。除结构,联合和数组之外的所有数据的对齐要求是对象的大小或当前打包大小(使用
/Zp
或pack pragma指定,以较小者为准)。
并且在/Zp (Struct Member Alignment)中指定了结构成员对齐的默认值
可用的包装值如下表所述:
/
Zp
论证效果 1个包含1字节边界的结构。与/ Zp相同。 2在2字节边界上打包结构。 4个字节边界上的4个结构。 8个包含8字节边界的结构(x86,ARM和ARM64的默认设置)。 16个包含16字节边界的结构(x64的默认值)。
由于x86的默认值为/ Zp8,即8字节,因此输出为16。
但是,您可以使用/Zp
选项指定不同的包装尺寸。
这是一个Live Demo与/Zp4
,输出为12而不是16。
结构的对齐是其最大成员的大小。
这意味着如果结构中有一个8字节(64位)的成员,那么结构将对齐到8个字节。
在您描述的情况下,如果编译器允许结构对齐到4个字节,则可能会导致在缓存行边界上有一个8字节的成员。
假设我们有一个具有16字节高速缓存行的CPU。考虑这样的结构:
struct Z
{
char s; // 1-4 byte
__int64 i; // 5-12 byte
__int64 i2; // 13-20 byte, need two cache line fetches to read this variable
};