为什么Rust优化器不能移除那些无用的指令(在Godbolt编译器浏览器上测试)?

15

我想查看一个微小的Rust函数的汇编输出:

pub fn double(n: u8) -> u8 {
    n + n
}

我使用了Godbolt Compiler Explorer来生成并查看汇编代码(当然,加了-O标志)。它显示出以下结果:

example::double:
    push    rbp
    mov     rbp, rsp
    add     dil, dil
    mov     eax, edi
    pop     rbp
    ret

我有些困惑,因为有几个指令似乎没有任何有用的作用:push rbpmov rbp, rsppop rbp。据我所知,仅执行这三个指令不会产生任何副作用。 那么为什么Rust优化器不会移除这些无用的指令呢?


作为比较,我还测试了C++版本

unsigned char doubleN(unsigned char n) {
    return n + n;
}

汇编输出(使用-O标志):

Translated text:

Assembly output (using -O flag):

doubleN(unsigned char): # @doubleN(unsigned char)
    add dil, dil
    mov eax, edi
    ret

事实上,在这里,我从优化的输出中所期望的那些“无用”的指令都已经被剔除了。


2
类似的问题已经被最近问过,但出于好的原因而被关闭。所以这个问题试图聚焦于并作为该主题的规范问题,正如其他人所建议的。 - Lukas Kalbertodt
1
尝试使用“-fomit-frame-pointer”编译。 - fuz
太遗憾了,LLVM在这里表现得很愚蠢。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
刚刚尝试了C++的等效版本,gcc做了我建议的事情,但是clang还是有些傻:https://godbolt.org/g/jdGSBp。(毫不奇怪,clang+llvm生成的汇编代码与rust+llvm完全相同) - Peter Cordes
1个回答

25
简短回答: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处理,除非您明确打开调试信息。

1
Rust有自己的调试信息格式或类似的东西吗?clangclang++默认启用-fomit-frame-pointer,即使启用了-g(调试),也可以这样做。您仍然可以在调试器中进行回溯,因为ABI要求在ELF可执行文件的单独部分中提供堆栈展开信息。(异常也使用它,因此即使剥离二进制文件,展开信息也在.eh_frame部分中) - Peter Cordes
@PeterCordes:Rust使用平台的调试格式,在Unix上使用DWARF,在Windows上使用PDB等... - Matthieu M.
1
@PeterCordes 是的,保留帧指针应该是不必要的。这个 是关于此问题的最新跟踪问题。但我猜优先级相当低,因为它只在调试模式下出现(而且大多数情况下已经非常慢了)。 - Lukas Kalbertodt
2
好的,这很有道理。只是尚未实现的编译器特性,而不是设计限制。 - Peter Cordes
2
虽然这对于早期的Rust版本是准确的,但在新版本的rustc中省略帧指针似乎不再取决于debuginfo自1.27以来:https://godbolt.org/z/j3qcvK9q4 - athre0z
显示剩余3条评论

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