我已经几次见过这种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之前,先选择堆栈对齐。