我已经几次见过这种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恢复为其原始值。

一些观察:


从整个功能来看,这似乎是一种完全round回的方式,只需将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 __tls_get_addr ABI错误实施变通方法时,出现了这个问题(GCC为堆栈对齐生成了不良代码),最终我们手工编写了堆栈重新对齐。

编辑还有另一个与RTL传递顺序有关的问题:在最终确定是否实际需要堆栈as BeeOnRope's second example shows之前,先选择堆栈对齐。

07-24 09:44
查看更多