gcc - 当需要额外的堆栈对齐时,gcc 奇怪的堆栈操作是怎么回事?

标签 gcc assembly x86 compiler-optimization

我见过这个 r10奇怪几次,所以让我们看看是否有人知道发生了什么。

以这个简单的函数为例:

#define SZ 4

void sink(uint64_t *p);

void andpop(const uint64_t* a) {
    uint64_t result[SZ];
    for (unsigned i = 0; i < SZ; i++) {
        result[i] = a[i] + 1;
    }

    sink(result);
}

它只是对传入数组的 4 个 64 位元素中的每个元素加 1 并将其存储在本地并调用 sink()结果(以避免整个功能被优化掉)。

这是corresponding集会:
andpop(unsigned long const*):
        lea     r10, [rsp+8]
        and     rsp, -32
        push    QWORD PTR [r10-8]
        push    rbp
        mov     rbp, rsp
        push    r10
        sub     rsp, 40
        vmovdqa ymm0, YMMWORD PTR .LC0[rip]
        vpaddq  ymm0, ymm0, YMMWORD PTR [rdi]
        lea     rdi, [rbp-48]
        vmovdqa YMMWORD PTR [rbp-48], ymm0
        vzeroupper
        call    sink(unsigned long*)
        add     rsp, 40
        pop     r10
        pop     rbp
        lea     rsp, [r10-8]
        ret

很难理解 r10 发生的几乎所有事情.一、r10设置为指向 rsp + 8 ,然后 push QWORD PTR [r10-8] ,据我所知,它会将返回地址的副本推送到堆栈上。随后,rbp正常设置然后最后r10本身被推。

为了放松这一切,r10从堆栈中弹出并用于恢复 rsp到它的原始值(value)。

一些观察:
  • 纵观整个函数,这一切似乎是一种完全迂回的简单恢复方式rsp到它之前的原始值 ret - 但通常的结尾 mov rsp, rpb也一样(见 clang)!
  • 也就是说,(昂贵的)push QWORD PTR [r10-8]在该任务中甚至没有帮助:这个值(返回地址?)显然从未使用过。
  • 为什么是 r10推和弹出所有?该值在非常小的函数体中没有被破坏,并且没有注册压力。

  • 那是怎么回事?之前看过好几次,一般都是想用r10 ,有时 r13 .这似乎与将堆栈对齐到 32 字节有关,因为如果您更改 SZ小于 4 它使用 xmm ops,问题就消失了。

    这是SZ == 2例如:
    andpop(unsigned long const*):
            sub     rsp, 24
            vmovdqa xmm0, XMMWORD PTR .LC0[rip]
            vpaddq  xmm0, xmm0, XMMWORD PTR [rdi]
            mov     rdi, rsp
            vmovaps XMMWORD PTR [rsp], xmm0
            call    sink(unsigned long*)
            add     rsp, 24
            ret
    

    好多了!

    最佳答案

    好吧,您已经回答了您的问题:堆栈指针需要对齐到 32 字节才能使用对齐的 AVX2 加载和存储进行访问,但 ABI 仅提供 16 字节对齐。由于编译器无法知道对齐有多少,它必须将堆栈指针保存在临时寄存器中并在之后恢复它。但是保存的值必须比函数调用更有效,因此必须将其放入堆栈,并且必须创建堆栈帧。

    一些 x86-64 ABI 有一个红色区域(信号处理程序不使用的堆栈指针下方的堆栈区域),因此对于此类短函数根本不更改堆栈指针是可行的,但 GCC 显然没有实现这个优化,无论如何它都不会在这里应用,因为最后是函数调用。

    此外,默认的堆栈对齐实现相当糟糕。对于这种情况,-maccumulate-outgoing-args使用 GCC 6 生成更好看的代码,只需在保存 RBP 后对齐 RSP,而不是在保存 RBP 之前复制返回地址:

    andpop:
            pushq   %rbp
            movq    %rsp, %rbp            # make a traditional stack frame
            andq    $-32, %rsp            # reserve 0 or 16 bytes
            subq    $32, %rsp
    
            vmovdqu (%rdi), %xmm0         # split unaligned load from tune=generic
            vinserti128     $0x1, 16(%rdi), %ymm0, %ymm0   # use -march=haswell instead
            movq    %rsp, %rdi
            vpaddq  .LC0(%rip), %ymm0, %ymm0
            vmovdqa %ymm0, (%rsp)
    
            vzeroupper
            call    sink@PLT
            leave
            ret
    

    (编者注:gcc8 和更高版本默认情况下使 asm 像这样( Godbolt compiler explorer with gcc8, clang7, ICC19, and MSVC ),即使没有 -maccumulate-outgoing-args )

    这个问题(GCC 为堆栈对齐生成了糟糕的代码)最近出现在我们不得不为 GCC 实现解决方法时 __tls_get_addr ABI 错误,我们最终手动编写了堆栈重新对齐。

    编辑 还有另一个问题,与 RTL pass ordering 相关:在最终确定是否确实需要堆栈之前选择堆栈对齐方式,as BeeOnRope's second example shows .

    关于gcc - 当需要额外的堆栈对齐时,gcc 奇怪的堆栈操作是怎么回事?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45423338/

    相关文章:

    assembly - Openvms/Itanium 程序集示例中的 "Hello World"?

    macos - 用户输入和输出在我的汇编代码中不起作用

    x86 - 对齐与未对齐 x86 SIMD 指令之间的选择

    c++ - g++ 发出难以理解的警告

    c++ - g++-8 和早期版本之间的奇怪行为

    assembly - 在子例程中使用 TRAP 例程? - LC3总成

    c - 为递归函数绘制堆栈框架

    performance - 我们什么时候应该使用预取?

    c - 严格的 C90 代码的 GCC 选项?

    c - BLAS sdot_ 函数返回意外结果