【问题标题】:Why does clang's epilogue use `add $N, %rsp` instead of `mov %rbp, %rsp` to restore `%rsp`?为什么 clang 的后记使用 `add $N, %rsp` 而不是 `mov %rbp, %rsp` 来恢复 `%rsp`?
【发布时间】:2021-11-21 21:35:24
【问题描述】:

考虑以下几点:

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()的最后,return之前,是恢复%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$ 

【问题讨论】:

    标签: assembly clang x86-64 micro-optimization


    【解决方案1】:

    如果它完全使用 RBP 作为帧指针,是的,mov %rbp, %rsp 会更紧凑,AFAIK 至少在所有 x86 微架构上都一样快。 (mov-elimination 可能甚至适用于它)。当 add 常量不适合 imm8 时更是如此。

    这可能是一个错过的优化,与https://bugs.llvm.org/show_bug.cgi?id=10319 非常相似(建议使用leave 而不是 mov/pop,这将在 Intel 上花费 1 个额外的 uop,但又节省了 3 个字节)。它指出,在正常情况下,整体静态代码大小的节省非常小,但并未考虑效率优势。在正常构建中(-O2 没有 -fno-omit-frame-pointer)只有少数函数会使用帧指针(仅在使用 VLA / alloca 或过度对齐堆栈时),因此可能的好处甚至更小。

    从那个错误看来,它只是 LLVM 懒得去寻找的一个窥视孔,因为许多函数还需要恢复其他寄存器,所以你实际上需要 add 一些其他值来将 RSP 指向其他推送下方.

    (GCC 有时使用mov 来恢复调用保留的regs,因此它可以使用leave。使用帧指针,这使得寻址模式相当紧凑,尽管4 字节的qword mov -8(%rbp), %r12 仍然是当然不会像 2 字节弹出那么小。如果我们没有帧指针(例如在 -O2 代码中),mov %rbp, %rsp 永远不是一个选项。)


    在考虑“不值得寻找”的理由之前,我想到了另一个小好处:

    调用保存/恢复 RBP 的函数后,RBP 为加载结果。所以在mov %rbp, %rsp 之后,未来使用 RSP 将需要等待该负载。可能一些极端情况最终会成为存储转发延迟的瓶颈,而寄存器修改只是 1 个周期。

    但总的来说,这似乎不太值得额外的代码大小;我希望这种极端情况很少见。虽然pop %rbp 需要新的 RSP 值,但调用者恢复的 RBP 值是我们返回后一连串加载的结果。 (幸运的是ret 有分支预测来隐藏延迟。)

    因此在某些基准测试中可能值得尝试两种方式;例如在一些标准基准(如 SPECint)上将此与 LLVM 的调整版本进行比较。

    【讨论】:

    • 谢谢,看来我们有这个bugs.llvm.org/show_bug.cgi?id=10319的副本
    • 我希望答案是,“因为那样你就可以使用 RBP 作为额外的寄存器”。 (显然,编译必须从 RSP 而不是 RBP 发出偏移量,但这在技术上很简单)。
    • @IraBaxter:这是使用clang -Os -fno-omit-frame-pointer 编译的。人们有时想要这样(而 IIRC 是 -Os GCC 默认值),所以如果您已经强制编译器将 RBP 浪费在作为帧指针上,那么您希望从中获得最大收益。但是,是的,既然你提到了,就调整措辞以提醒读者大多数软件都是在没有-fno-omit-frame-pointer 的情况下构建的。)
    猜你喜欢
    • 2021-07-30
    • 2021-04-18
    • 1970-01-01
    • 2021-11-15
    • 2016-07-31
    • 1970-01-01
    • 1970-01-01
    • 2012-09-20
    • 2012-06-15
    相关资源
    最近更新 更多