如何在GCC中将堆栈对齐到32字节边界?

11

我正在使用基于GCC 4.6.1的MinGW64构建来为Windows 64位目标进行开发。我正在尝试使用新的英特尔AVX指令,我的命令行参数是-march=corei7-avx -mtune=corei7-avx -mavx

然而,当在堆栈上分配局部变量时,我遇到了分段错误(segmentation fault)问题。GCC使用对齐移动指令VMOVAPSVMOVAPD来移动__m256__m256d型别,这些指令要求32字节对齐。然而,Windows 64位的栈仅有16字节对齐。

如何将GCC的堆栈对齐改为32字节?

我已经尝试过使用-mstackrealign,但无济于事,因为它只能对齐到16字节。我也无法让__attribute__((force_align_arg_pointer))有效,因为它仍然只会对齐到16个字节。我还没有找到其他可以解决此问题的编译器选项。非常感谢任何帮助。

编辑: 我尝试使用-mpreferred-stack-boundary=5,但GCC说不支持5这个数值。我已经没有其他想法了。


1
这是否意味着__attribute__ ((aligned (32)))也不被尊重?例如,如果您使用__m256 x __attribute__ ((aligned (32))) - nos
1
Linux也不会将堆栈对齐到32。针对Linux的gcc使用and $-32, %rsp(或任何更高的对齐方式)来对需要溢出__m256__m512或任何您使用alignas(32)声明的对象或任何高于16的对象的函数进行堆栈对齐。MinGW gcc没有使用相同的序列来保存原始的rsp并对其进行对齐,这似乎是一个奇怪的错误。 - Peter Cordes
3个回答

17

我一直在研究这个问题,已经提交了GCC缺陷报告,并发现这是一个与MinGW64相关的问题。请参见GCC Bug#49001。显然,在Windows上,GCC不支持32字节堆栈对齐。这有效地阻止了使用256位AVX指令。

我研究了几种应对此问题的方法。最简单而最粗暴的解决方案是用未对齐的替代品VMOVUPS等替换对齐内存访问VMOVAPS/PD/DQA。因此,我昨晚学习了Python(顺便说一句,非常好用的工具),并编写了以下脚本,可以处理由GCC生成的输入汇编文件:

import re
import fileinput
import sys

# fix aligned stack access
# replace aligned vmov* by unaligned vmov* with 32-byte aligned operands 
# see Intel's AVX programming guide, page 39
vmova = re.compile(r"\s*?vmov(\w+).*?((\(%r.*?%ymm)|(%ymm.*?\(%r))")
aligndict = {"aps" : "ups", "apd" : "upd", "dqa" : "dqu"};
for line in fileinput.FileInput(sys.argv[1:],inplace=1):
    m = vmova.match(line)
    if m and m.group(1) in aligndict:
        s = m.group(1)
        print line.replace("vmov"+s, "vmov"+aligndict[s]),
    else:
        print line,

这种方法非常安全可靠。尽管我偶尔会遇到性能惩罚的情况。当堆栈不对齐时,内存访问会跨越缓存行边界。幸运的是,大部分时间代码的执行速度与对齐访问一样快。我建议在关键循环中将函数内联!

我还尝试使用另一个Python脚本修复每个函数 prolog 中的堆栈分配,试图始终将其对齐到32字节边界。这似乎对某些代码有效,但对其他代码则无效。我必须依靠GCC的良好意愿来分配对齐的局部变量(相对于堆栈指针),它通常会这样做。但这并非总是如此,特别是在需要在函数调用之前保存所有ymm寄存器的情况下发生严重的寄存器溢出。(所有的ymm寄存器都是由被调用者保存的)。如果感兴趣,我可以发布这个脚本。

最好的解决方案是修复GCC MinGW64构建。不幸的是,我对其内部工作一无所知,上周刚开始使用它。


你能分享一下你的Prolog重写脚本吗?另外,如何从汇编文件(由-S生成)得到可执行文件?谢谢。 - user1649948
@NobertP。MinGW64的后续版本是否有所改善? - Royi
2
由于GCC似乎正在掩盖这个漏洞(它已经存在6年了!),我们决定走另一条路线。一个老式的请愿书,请签名。 https://www.change.org/p/gnu-project-gcc-compiler-fix-bug-54412 - ichad.c
MinGW GCC 确实支持在堆栈上使用超对齐类型,例如alignas(32) int foo[8];。如果您查看汇编代码,您会看到其中有and rsp, -32。但是当仅存在__m256 / __m256i这样的类型时,它无法对齐堆栈指针。 - Peter Cordes

1
我遇到了同样的问题,在使用AVX时函数会出现分段错误。这也是由于堆栈未对齐导致的。考虑到这是编译器问题(在Windows中无法使用可帮助解决问题的选项),我通过以下方式解决了堆栈使用问题:
  1. 使用静态变量(请参见此issue)。由于它们并不存储在堆栈中,您可以通过在声明中使用__attribute__((align(32)))来强制对其进行对齐。例如:static __m256i r __attribute__((aligned(32)))

  2. 内联接收/返回AVX数据的函数/方法。您可以通过在函数原型/声明中添加inline__attribute__((always_inline))来强制GCC内联您的函数/方法。内联函数将增加程序的大小,但它们也可以防止函数使用堆栈(因此,避免了堆栈对齐问题)。例如:inline __m256i myAvxFunction(void) __attribute__((always_inline));

请注意,静态变量的使用是不安全的,正如参考文献中所提到的。如果您正在编写多线程应用程序,则可能需要为关键路径添加一些保护措施。

在 macOS 中,编译器会将任何数组对齐到 16 字节。在 64 位系统上,GCC 是否也会这样做呢? - Royi
嗨。在64位Windows计算机上使用GCC进行实验后,我发现数组的第一个元素默认情况下是16字节对齐的。数组的其余元素根据数组元素的数据类型而异。例如,一个_n_ char(1字节宽)的数组A将具有&A[n] =&A[0] +_n_,其中&A[n]将会是16字节对齐的。 - Ricardo Alejos
MinGW64的后续版本是否带有GCC 7.x,解决了这个问题? - Royi

1

您可以通过以下方式实现所需的效果:

  1. 将变量声明为结构体中的字段,而不是变量
  2. 声明一个比结构体大适当数量填充的数组
  3. 进行指针/地址算术运算,以在数组内找到32字节对齐的地址
  4. 将该地址转换为指向您的结构体的指针
  5. 最后使用您的结构体的数据成员

当malloc()未正确对齐堆上的内容时,您也可以使用相同的技术。

例如:

void foo() {
    struct I_wish_these_were_32B_aligned {
          vec32B foo;
          char bar[32];
    }; // not - no variable definition, just the struct declaration.
    unsigned char a[sizeof(I_wish_these_were_32B_aligned) + 32)];
    unsigned char* a_aligned_to_32B = align_to_32B(a);
    I_wish_these_were_32B_aligned* s = (I_wish_these_were_32B_aligned)a_aligned_to_32B;
    s->foo = ...
}

在哪里

unsigned char* align_to_32B(unsiged char* a) {
     uint64_t u = (unit64_t)a;
     mask_aligned32B = (1 << 5) - 1;
     if (u & mask_aligned32B == 0) return (unsigned char*)u;
     return (unsigned char*)((u|mask_aligned_32B) + 1);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接