简短回答:Godbolt添加了一个
-C debuginfo=1
标志,强制优化器保留所有管理帧指针的指令。Rust在编译时移除这些指令,当进行优化且没有调试信息时。
这些指令是用来做什么的?
这三个指令是函数序言和尾声的一部分。在这里,它们特别管理所谓的帧指针或基指针(x86_64上的rbp
)。注意:不要将基指针与堆栈指针(x86_64上的rsp
)混淆!基指针始终指向当前堆栈帧内部:
┌──────────────────────┐
│ function arguments │
│ ... │
├──────────────────────┤
│ return address │
├──────────────────────┤
[rbp] ──> │ last rbp │
├──────────────────────┤
│ local variables │
│ ... │
└──────────────────────┘
有趣的是,基指针指向堆栈中存储
rbp
最后一个值的内存片段。这意味着我们可以轻松找到上一个堆栈帧(即调用“我们”的函数的帧)的基指针。
更好的是:所有基指针形成类似于链接列表的东西!我们可以轻松地遵循所有
last rbp
以向上遍历堆栈。这意味着在程序执行的每个点,我们都知道哪些函数调用了其他函数,以便我们最终到达“这里”。
让我们再次查看指令:
; We store the "old" rbp on the stack
push rbp
; We update rbp to hold the new value
mov rbp, rsp
; We undo what we've done: we remove the old rbp
; from the stack and store it in the rbp register
pop rbp
这些指令有什么用处?
基指针及其“链表”属性对于调试和分析程序行为非常重要(例如性能分析)。没有基指针,生成堆栈跟踪和定位当前执行的函数就更加困难。
此外,管理帧指针通常不会显著减慢速度。
为什么优化器不会删除它们,我该如何强制执行?
如果Godbolt没有向编译器传递-C debuginfo=1
,则它们通常会被删除。这会告诉编译器保留所有与帧指针处理相关的内容,因为我们需要它来进行调试。请注意,帧指针并不是调试所必需的 -- 其他类型的调试信息通常足够。在存储任何类型的调试信息时都会保留帧指针,因为在 Rust 程序中删除帧指针仍存在一些小问题。这正在讨论中的 GitHub 跟踪问题中讨论。
您可以通过自己添加标志-C debuginfo=0
来“撤消”它。这将产生与 C++ 版本完全相同的输出:
example::double:
add dil, dil
mov eax, edi
ret
您也可以通过执行以下命令在本地进行测试:
$ rustc -O --crate-type=lib --emit asm -C "llvm-args=-x86-asm-syntax=intel" example.rs
使用优化编译(
-O
)会自动删除
rbp
处理,除非您明确打开调试信息。
mov eax, edi
/add al,al
可以避免在Intel Core2 / Nehalem上出现部分寄存器停顿(即使使用-C ar = core2
这个 Rust 代码的相当于clang -march=core2
,它仍然会生成这种指令)。如果Rust使用与C相同的x86-64 System V ABI,则窄返回值可以在高字节中留下垃圾,因此lea eax,[rdi + rdi]
可以起作用。 - Peter Cordes