为什么 clang 的尾声使用 add $N, %rsp 而不是 mov %rbp, %rsp 来恢复 %rsp?
Why does clang's epilogue use `add $N, %rsp` instead of `mov %rbp, %rsp` to restore `%rsp`?
考虑以下几点:
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 [=10=]x1000,%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 [=10=]x1000,%rsp
2e: 5d pop %rbp
2f: c3 ret
ammarfaizi2@integral:/tmp$
在a_func()
的结尾,return之前,是恢复%rsp
的函数尾声。它使用 add [=14=]x1000, %rsp
产生 48 81 c4 00 10 00 00
.
它不能只使用只产生 3 个字节的 mov %rbp, %rsp
48 89 ec
吗?
为什么 clang 不使用更短的方式 (mov %rbp, %rsp
)?
考虑到代码大小的权衡,使用 add [=14=]x1000, %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 [=11=]x1000,%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 [=11=]x1000,%rsp
21: 5d pop %rbp
22: c3 ret
ammarfaizi2@integral:/tmp$
如果它完全使用 RBP 作为帧指针,是的,mov %rbp, %rsp
会更紧凑并且 AFAIK 至少在所有 x86 微体系结构上一样快。 (移动消除甚至可能适用于它)。当 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
来恢复调用保留的 reg,因此它可以使用 leave
。使用帧指针,这使得寻址模式相当紧凑以进行编码,尽管 4 字节qword mov -8(%rbp), %r12
当然仍然没有 2-byte pop 小。如果我们没有帧指针(例如在 -O2
代码中),mov %rbp, %rsp
永远不是一个选项。 )
在考虑“不值得找”的理由之前,我想到了另一个小好处:
调用了saves/restoresRBP的函数后,RBP是加载结果。因此在 mov %rbp, %rsp
之后,未来使用 RSP 将需要等待该负载。可能某些极端情况最终会在存储转发延迟方面成为瓶颈,而寄存器修改仅为 1 个周期。
但总的来说,这似乎不太值得额外的代码大小;我希望这种极端情况很少见。尽管 pop %rbp
需要新的 RSP 值,因此调用者恢复的 RBP 值是我们 return 之后的两个负载链的结果。 (幸运的是 ret
有分支预测来隐藏延迟。)
所以在某些基准测试中可能值得尝试这两种方式;例如在一些标准基准测试(如 SPECint)上将此与 LLVM 的调整版本进行比较。
考虑以下几点:
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 [=10=]x1000,%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 [=10=]x1000,%rsp
2e: 5d pop %rbp
2f: c3 ret
ammarfaizi2@integral:/tmp$
在a_func()
的结尾,return之前,是恢复%rsp
的函数尾声。它使用 add [=14=]x1000, %rsp
产生 48 81 c4 00 10 00 00
.
它不能只使用只产生 3 个字节的 mov %rbp, %rsp
48 89 ec
吗?
为什么 clang 不使用更短的方式 (mov %rbp, %rsp
)?
考虑到代码大小的权衡,使用 add [=14=]x1000, %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 [=11=]x1000,%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 [=11=]x1000,%rsp
21: 5d pop %rbp
22: c3 ret
ammarfaizi2@integral:/tmp$
如果它完全使用 RBP 作为帧指针,是的,mov %rbp, %rsp
会更紧凑并且 AFAIK 至少在所有 x86 微体系结构上一样快。 (移动消除甚至可能适用于它)。当 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
来恢复调用保留的 reg,因此它可以使用 leave
。使用帧指针,这使得寻址模式相当紧凑以进行编码,尽管 4 字节qword mov -8(%rbp), %r12
当然仍然没有 2-byte pop 小。如果我们没有帧指针(例如在 -O2
代码中),mov %rbp, %rsp
永远不是一个选项。 )
在考虑“不值得找”的理由之前,我想到了另一个小好处:
调用了saves/restoresRBP的函数后,RBP是加载结果。因此在 mov %rbp, %rsp
之后,未来使用 RSP 将需要等待该负载。可能某些极端情况最终会在存储转发延迟方面成为瓶颈,而寄存器修改仅为 1 个周期。
但总的来说,这似乎不太值得额外的代码大小;我希望这种极端情况很少见。尽管 pop %rbp
需要新的 RSP 值,因此调用者恢复的 RBP 值是我们 return 之后的两个负载链的结果。 (幸运的是 ret
有分支预测来隐藏延迟。)
所以在某些基准测试中可能值得尝试这两种方式;例如在一些标准基准测试(如 SPECint)上将此与 LLVM 的调整版本进行比较。