为什么“对齐”在32位和64位系统上相同?

时间:2019-04-30 11:38:24

标签: c++ visual-c++ 32bit-64bit memory-alignment abi

我想知道编译器是否会在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;
};

所以我可以知道这是什么原因吗?

  1. 要与64位系统进行前向兼容?
  2. 由于在32位处理器上支持64位数字的限制?

4 个答案:

答案 0 :(得分:12)

填充不是由字长决定的,而是由每种数据类型的the alignment确定的。

在大多数情况下,对齐要求等于类型的大小。因此,对于int64这样的64位类型,您将获得8字节(64位)对齐。需要将填充插入结构,以确保该类型的存储最终位于正确对齐的地址。

在两种架构上使用大小分别为 的内置数据类型(例如指针类型(int*)时,您可能会看到32位和64位之间的填充差异。 / p>

答案 1 :(得分:8)

这是有关数据类型的对齐要求的问题,如 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。

答案 2 :(得分:4)

每个原始类型的

Size和alignof()(任何 类型的对象必须具有的最小对齐)是ABI 1 设计选择与体系结构的寄存器宽度分开。

结构填充规则也可能比仅将每个struct成员对齐到其在结构内部的最小对齐更为复杂。这是ABI的另一部分。

面向32位x86的

MSVC将__int64最小对齐方式设为4,但是其默认结构打包规则将结构中的类型相对于min(8, sizeof(T))对齐(仅适用于非集合类型)。这不是 的直接引号,这是我对@ P.W的答案中的MSVC docs link的解释,基于MSVC的实际作用。 (我怀疑文本中的“较小者”应该在括号之外,但也许他们对编译指示和命令行选项上的交互有不同的看法?)

(包含char[8]的8字节结构仍然只能在另一个结构内部进行1字节对齐,或者包含alignas(16)成员的结构仍可以在另一个结构内部进行16字节对齐。)< / p>

请注意,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字节对齐的内存,因此对于动态分配的对象(这很常见)很有意义。 )。强制将堆栈上的局部变量完全对齐会增加对齐堆栈指针的成本,但是通过将结构布局设置为利用8字节对齐的存储,我们可以获得静态和动态存储的优势。


这也可能旨在获取32位和64位代码,以在共享内存的某些结构布局上达成共识。 (但是请注意,x86-64的默认值为min(16, sizeof(T)),因此,如果有任何16字节类型的数据不是聚合的(结构/联合/数组),并且它们不没有alignas。)


最小绝对对齐数4是32位代码可以假定的4字节堆栈对齐方式。在静态存储中,编译器将为var选择最多8或16个字节的自然对齐方式在结构外部,以便使用SSE2向量进行有效复制。

在较大的功能中,出于性能方面的考虑,MSVC可能决定将堆栈对齐8。用于堆栈上的double变量,实际上可以用单个指令进行操作,或者也可以用于带有SSE2向量的int64_t。请参阅此2006年文章中的 Stack Alignment 部分:Windows Data Alignment on IPF, x86, and x64。因此,在32位代码中,您不能依靠自然地对齐int64_t*double*

(我不确定MSVC是否会自行创建对齐程度更低的int64_tdouble对象。如果您使用#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) = 4sizeof(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至少即将出现。奔腾具有64位宽的数据总线,,因此它的FPU实际上可以在64位对齐的 缓存中加载64位double

>

或者对于fild / fistp,在与double之间进行转换时,加载/存储64位整数。有趣的事实:由于奔腾(Pentium):Why is integer assignment on a naturally aligned variable atomic on x86?

,在x86上保证自然对齐的访问最多64位

脚注1 :ABI还包含一个调用约定,或者在MS Windows的情况下,可以选择各种调用约定,您可以使用__fastcall之类的函数属性进行声明,但是诸如long long之类的基本类型的大小和对齐要求也是编译器必须达成共识才能使函数可以相互调用的。 (ISO C ++标准仅讨论单个“ C ++实现”; ABI标准是“ C ++实现”如何使其彼此兼容的方法。)

请注意,结构布局规则也是ABI的一部分:编译器必须就结构布局达成共识,以创建兼容的二进制文件,这些二进制文件会传递结构或指向结构的指针。否则,s.x = 10; foo(&x);相对于结构的基址可能写入的偏移量与期望单独读取的foo()(可能在DLL中)不同。


脚注2

GCC也存在此C ++ alignof()错误,直到在为C11 _Alignof()修复后fixed in 2018 for g++8为止。请参阅该错误报告,以根据该标准的引用进行一些讨论,得出的结论是alignof(T)应该真正报告您可以看到的最低保证对齐方式,不是您想要的性能首选对齐方式。也就是说,使用少于int64_t*对齐的alignof(int64_t)是不确定的行为。

(通常在x86上可以正常工作,但是假设整数int64_t迭代将达到16或32字节对齐边界的矢量化可能会出错。有关gcc的示例,请参见Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?

gcc错误报告讨论了i386 System V ABI,它具有与MSVC不同的结构打包规则:基于最小对齐方式,不是首选。但是现代的i386 System V保持16字节的堆栈对齐,因此,编译器曾经创建过int64_t和{的结构内部只有 (由于ABI的结构打包规则) {1}}个小于自然对齐的对象。无论如何,这就是GCC错误报告在讨论结构成员作为特殊情况的原因。

与带有MSVC的32位Windows截然相反,在Windows中,结构打包规则与double兼容,但是堆栈上的本地变量总是潜在地未对齐,除非您使用alignof(int64_t) == 8来专门请求对齐。

32位MSVC具有alignas()alignas(int64_t) int64_t tmp不同的奇怪现象,并发出额外的指令来对齐堆栈。这是因为int64_t tmp;就像alignas(int64_t),比实际最小值更对齐。

alignas(8)

(32位)x86 MSVC 19.20 -O2这样编译( on Godbolt ,还包括32位GCC和结构测试用例):

void extfunc(int64_t *);

void foo_align8(void) {
    alignas(int64_t) int64_t tmp;
    extfunc(&tmp);
}

但是如果没有_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 而不是LEA / push;这是次要的优化遗漏。

将指针传递给非内联函数可证明它不仅仅是局部弯曲规则。其他一些仅以push esp作为arg的函数必须处理此可能未对齐的指针,而无需获取有关其来源的任何信息。

如果int64_t*确实是 8,则该函数可能会在asm中以错误对齐指针的方式手写。或者,也可以使用C {SE1内部函数,例如alignof(int64_t)用C编写,在处理0或1个元素以达到对齐边界之后,它们需要16字节对齐。

但是,根据MSVC的实际行为,_mm_load_si128()数组元素可能都不按16对齐,因为它们全部跨越8字节边界。


顺便说一句,我不建议直接使用像int64_t这样的特定于编译器的类型。您可以使用int64_t from <cstdint>(也称为__int64)来编写可移植代码。

在MSVC中,<stdint.h>int64_t的类型相同。

在其他平台上,通常为__int64longlong long确保完全为64位,没有填充,如果有的话,则为2的补码。 (这是所有针对常规CPU的明智的编译器。C99和C ++要求int64_t至少为64位,并且在具有8位字节和2的幂的寄存器的机器上,long long通常是完全64位,可以用作long long。或者,如果int64_t是64位类型,那么long可以将其用作typedef。)

我假设<cstdint>__int64在MSVC中是相同的类型,但是MSVC不会强制执行严格的别名,因此它们是否是相同的类型并不重要,只是他们使用相同的表示形式。

答案 3 :(得分:-2)

结构的对齐方式是其最大成员的大小。

这意味着,如果结构中有一个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
};