为什么clang的函数结尾要使用`add $N, %rsp`而不是`mov %rbp, %rsp`来恢复`%rsp`?

3

考虑以下内容:

ammarfaizi2@integral:/tmp$ vi test.c
ammarfaizi2@integral:/tmp$ cat test.c

extern void use_buffer(void *buf);

void a_func(void)
{
    char buffer[4096];
    use_buffer(buffer);
}

__asm__("emit_mov_rbp_to_rsp:\n\tmovq %rbp, %rsp");

ammarfaizi2@integral:/tmp$ clang -Wall -Wextra -c -O3 -fno-omit-frame-pointer test.c -o test.o
ammarfaizi2@integral:/tmp$ objdump -d test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <emit_mov_rbp_to_rsp>:
   0: 48 89 ec              mov    %rbp,%rsp
   3: 66 2e 0f 1f 84 00 00  cs nopw 0x0(%rax,%rax,1)
   a: 00 00 00 
   d: 0f 1f 00              nopl   (%rax)

0000000000000010 <a_func>:
  10: 55                    push   %rbp
  11: 48 89 e5              mov    %rsp,%rbp
  14: 48 81 ec 00 10 00 00  sub    $0x1000,%rsp
  1b: 48 8d bd 00 f0 ff ff  lea    -0x1000(%rbp),%rdi
  22: e8 00 00 00 00        call   27 <a_func+0x17>
  27: 48 81 c4 00 10 00 00  add    $0x1000,%rsp
  2e: 5d                    pop    %rbp
  2f: c3                    ret    
ammarfaizi2@integral:/tmp$ 

a_func() 结束前,在返回之前,函数的结尾部分是恢复 %rsp。它使用语句 add $0x1000, %rsp,其结果为 48 81 c4 00 10 00 00
它难道不能只使用语句 mov %rbp, %rsp,其结果只有 3 个字节 48 89 ec 吗?
为什么 clang 不使用更短的方式(mov %rbp, %rsp)呢?
在代码大小方面的权衡中,使用 add $0x1000, %rsp 而不是 mov %rbp, %rsp 的优点是什么?
更新(额外内容)
即使使用了 -Os,仍会生成相同的代码。因此,我认为有一个合理的理由避免使用 mov %rbp, %rsp
ammarfaizi2@integral:/tmp$ clang -Wall -Wextra -c -Os -fno-omit-frame-pointer test.c -o test.o
ammarfaizi2@integral:/tmp$ objdump -d test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <emit_mov_rbp_to_rsp>:
   0:   48 89 ec                mov    %rbp,%rsp

0000000000000003 <a_func>:
   3:   55                      push   %rbp
   4:   48 89 e5                mov    %rsp,%rbp
   7:   48 81 ec 00 10 00 00    sub    $0x1000,%rsp
   e:   48 8d bd 00 f0 ff ff    lea    -0x1000(%rbp),%rdi
  15:   e8 00 00 00 00          call   1a <a_func+0x17>
  1a:   48 81 c4 00 10 00 00    add    $0x1000,%rsp
  21:   5d                      pop    %rbp
  22:   c3                      ret    
ammarfaizi2@integral:/tmp$ 
1个回答

6
如果整个过程中使用RBP作为帧指针,是的,mov %rbp, %rsp在所有x86微架构上都更加紧凑且至少与其速度相当(移除mov可能仍然有效)。特别是当添加的常数不适合imm8时。这可能是一个被忽视的优化,与https://bugs.llvm.org/show_bug.cgi?id=10319非常相似(提议使用leave代替mov/pop,在英特尔上将多花费1个额外的微操作,但可以节省另外3个字节),它指出了普通情况下的整体静态代码大小节省非常小。但并未考虑效率方面的好处。-O2没有-fno-omit-frame-pointer正常构建时,只有一些函数会使用帧指针(仅在使用VLA/alloca或对齐堆栈时)因此可能的好处更小。
从该错误报告看来,LLVM似乎不打算寻找这个窥孔,因为许多函数还需要恢复其他寄存器,因此实际上需要添加一些其他值来使RSP指向其他推送以下。
(GCC有时使用mov来恢复被调用保留的寄存器,以便可以使用leave。在具有帧指针的情况下,编码时地址模式相当紧凑,尽管4字节qword mov -8(%rbp),%r12 当然不如2字节pop小。如果我们没有帧指针(例如在-O2代码中),mov %rbp, %rsp从来不是一个选项。)
考虑“不值得寻找”的原因之前,我想到了另一个次要的好处:
在调用保存/恢复RBP的函数之后,RBP是一个载入结果。因此,在mov %rbp,%rsp之后,将来对RSP的使用需要等待该载入操作。可能某些边缘情况最终会受到存储转发延迟的限制,而不是只需修改寄存器1个周期。
但是,这似乎通常不值得额外的代码大小;我预计这样的边缘情况很少见。尽管该新的RSP值需要pop %rbp,因此调用者的已恢复RBP值是我们返回后两次加载的一系列结果。(幸运的是,ret具有分支预测以隐藏延迟。)
因此,可能值得在一些基准测试中尝试两种方式;例如将其与SPECint等标准基准测试的LLVM版本进行比较。

2
谢谢,看起来我们有这个的重复 https://bugs.llvm.org/show_bug.cgi?id=10319 - Ammar Faizi
我本来期望的答案是,“因为你可以将RBP用作额外的寄存器”。(显然,编译器必须从RSP而不是RBP发出偏移量,但这在技术上很简单)。 - Ira Baxter
1
@IraBaxter:这是使用clang -Os -fno-omit-frame-pointer编译的。有时候人们会想要它(如果我没记错的话,这是-Os GCC的默认设置),所以如果你已经强制编译器把RBP浪费在作为一个框架指针上,你要充分利用它的最大好处。但是,既然你提到了它,调整措辞以提醒读者大多数软件是没有使用-fno-omit-frame-pointer构建的。 - Peter Cordes

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