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

新手上路,请多包涵

我想知道编译器是否会在 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 位数字的限制?

原文由 Shen Yuan 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 828
2 个回答

每个基本类型的大小和 alignof() (该类型的任何对象 必须 具有的最小对齐)是与架构的寄存器宽度分开的 ABI 1设计选择。

结构打包规则也可能比将每个结构成员对齐到结构内部的最小对齐更复杂;这是 ABI 的另一部分。

面向 32 位 x86 的 MSVC 给 __int64 最小 对齐为 4,但 其默认结构打包规则将结构内的类型对齐到 min(8, sizeof(T)) 相对于结构的开头。 (仅适用于非聚合类型)。这 不是 直接引用,这是我根据 MSVC 似乎实际所做的对 @PW 答案中的 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() 指定它成员来暗示该类型。 (例如,函数内部堆栈上的本地 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 变量实际上可以用单个指令进行操作,或者也可能用于 int64_t 与 SSE2 向量。请参阅这篇 2006 年文章中的 堆栈对齐 部分: IPF、x86 和 x64 上的 Windows 数据对齐。因此,在 32 位代码中,您不能依赖 int64_t*double* 自然对齐。

(我不确定 MSVC 是否会创建更不对齐的 int64_t-Zp1 double 对象。如果你使用 #pragma pack 1 肯定是的 --- ,但这会改变 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 时,Pentium CPU 至少已经出现。 Pentium 有 64 位宽的数据总线, 所以它的 FPU 如果 它是 64 位对齐的,那么它真的可以在单个缓存访问中加载 64 位 double

或者对于 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() 错误,直到在为 C11 _Alignof() 修复一段时间后,它 在 2018 年为 g++8 修复。请参阅基于标准引用的一些讨论的错误报告,该标准得出的结论是 alignof(T) 应该真正报告您可以看到的最小保证对齐, 而不是 您想要的性能首选对齐。即使用 int64_t* 小于 alignof(int64_t) 对齐是未定义的行为。

(它通常可以在 x86 上正常工作,但是假设整数 int64_t 迭代将达到 16 或 32 字节对齐边界的向量化可能会出错。请参阅 为什么对 mmap 内存的未对齐访问有时会出现段错误在 AMD64 上? 以 gcc 为例。)

gcc 错误报告讨论了 i386 System V ABI,它具有与 MSVC 不同的结构打包规则:基于最小对齐,而不是首选。但是现代 i386 System V 维护 16 字节堆栈对齐,因此编译器 在结构内部(因为结构打包规则是 ABI 的一部分)创建 int64_tdouble 不是自然对齐的对象。无论如何,这就是 GCC 错误报告将 struct 成员作为特例讨论的原因。

与带有 MSVC 的 32 位 Windows 相反,其中结构打包规则与 alignof(int64_t) == 8 兼容,但堆栈上的局部变量总是可能未对齐,除非您使用 alignas() 来特别要求对齐。

32 位 MSVC 具有 alignas(int64_t) int64_t tmpint64_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 和 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/push;这是一个小的错过优化。

将指针传递给非内联函数证明它不仅仅是在本地弯曲规则。其他一些只获得 int64_t* 作为 arg 的函数必须处理这个潜在的欠对齐指针,而没有得到任何关于它来自哪里的信息。

如果 alignof(int64_t) 真的是 8,那么该函数可以在 asm 中以一种在未对齐指针上出错的方式手写。或者它可以用 SSE2 内在函数(如 _mm_load_si128() )用 C 编写,在处理 0 或 1 个元素以达到对齐边界后,需要 16 字节对齐。

但根据 MSVC 的实际行为,可能没有 int64_t 数组元素按 16 对齐,因为它们 跨越 8 字节边界。


顺便说一句,我不建议直接使用编译器特定类型,如 __int64 。您可以使用 来自 <cstdint> int64_t 来编写可移植代码,也就是 <stdint.h>

在 MSVC 中, int64_t 将与 __int64 的类型相同。

在其他平台上,它通常是 longlong longint64_t 保证完全是 64 位,没有填充和 2 的补码,如果提供的话。 (这是针对普通 CPU 的所有理智编译器。C99 和 C++ 要求 long long 至少为 64 位,并且在具有 8 位字节和寄存器为 2 的幂的机器上, long long is normally exactly 64 bits and can be used as int64_t . Or if long is a 64-bit type, then <cstdint> might use that as类型定义。)

我假设 __int64long long 在 MSVC 中是相同的类型,但是 MSVC 并不强制执行严格混叠,所以它们是否是完全相同的类型并不重要,只是他们使用相同的表示。

原文由 Peter Cordes 发布,翻译遵循 CC BY-SA 4.0 许可协议

填充不是由字长决定的,而是由每种数据类型 的对齐方式 决定的。

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

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

原文由 ComicSansMS 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题