gpt4 book ai didi

c++ - 为什么 “alignment”在32位和64位系统上相同?

转载 作者:行者123 更新时间:2023-12-03 00:59:22 25 4
gpt4 key购买 nike

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

所以我可以知道这背后的原因是什么?
  • 是否与64位系统具有前向兼容性?
  • 由于在32位处理器上支持64位数字的限制?
  • 最佳答案

    每个基本类型的大小和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字节对齐。)

    请注意,ISO C++不保证基本类型具有alignof(T) == sizeof(T)还请注意,MSVC的 alignof() 定义与ISO C++标准不匹配:MSVC表示alignof(__int64) == 8,但是某些__int64对象的对齐方式比该对齐方式少。

    因此,令人惊讶的是,即使MSVC并不总是麻烦确保结构本身具有超过4字节的对齐方式,我们也会得到额外的填充,除非您在变量或struct成员上使用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 vector 进行有效复制。

    在较大的功能中,出于性能方面的考虑,MSVC可能决定将堆栈对齐8。用于堆栈上的double变量,实际上可以使用单个指令进行操作,或者也可以用于带有SSE2 vector 的int64_t。请参阅此2006年文章中的“堆栈对齐”部分: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。这些选择

    在结构之外(例如,堆栈上的全局var或局部变量),现代的32位编译器确实选择将int64_t与8字节边界对齐以提高效率(因此可以使用MMX或SSE2 64位加载进行加载/复制)或x87 fild进行int64_t->双重转换)。

    这就是为什么现代版本的i386 System V ABI保持16字节堆栈对齐的原因之一:因此8字节和16字节对齐的本地变量是可能的。

    在设计32位Windows ABI时,奔腾CPU至少即将出现。奔腾具有64位宽数据总线,因此,如果它是64位对齐的,它的FPU实际上可以在单个缓存访问中加载64位double

    或对于fild/fistp,当从double转换为/时,加载/存储一个64位整数。有趣的事实:由于奔腾: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);(可能在DLL中),foo()相对于该结构的基数可能写入的偏移量与预期的不同。

    脚注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字节的堆栈对齐,因此,仅在结构内部(由于ABI包含结构打包规则),编译器才能创建比自然对齐少的int64_tdouble对象。无论如何,这就是为什么GCC错误报告将结构成员作为特殊情况进行讨论的原因。

    与带有MSVC的32位Windows截然相反,在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像这样编译( 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/push;这是次要的优化遗漏。

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

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

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

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

    在MSVC中, int64_t__int64的类型相同。

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

    我假设 __int64long long在MSVC中是相同的类型,但是MSVC无论如何都不执行严格的别名,因此它们是否是相同的类型并不重要,只是它们使用相同的表示形式。

    关于c++ - 为什么 “alignment”在32位和64位系统上相同?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55920103/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com