为什么Rust优化器不会删除那些无用的指令(在Godbolt Compiler Explorer上测试)?

Luk*_*odt 11 assembly x86-64 rust llvm-codegen

我想看看一个小的Rust函数的程序集输出:

pub fn double(n: u8) -> u8 {
    n + n
}
Run Code Online (Sandbox Code Playgroud)

我使用Godbolt Compiler Explorer来生成和查看程序集(-O当然还有标志).它显示了这个输出:

example::double:
    push    rbp
    mov     rbp, rsp
    add     dil, dil
    mov     eax, edi
    pop     rbp
    ret
Run Code Online (Sandbox Code Playgroud)

现在我有点困惑,因为有这似乎并没有做任何有用的几个指令:push rbp,mov rbp, rsppop rbp.根据我的理解,我认为单独执行这三个指令没有任何副作用.那么为什么Rust优化器不会删除那些无用的指令呢?


为了比较,我还测试了一个C++版本:

unsigned char doubleN(unsigned char n) {
    return n + n;
}
Run Code Online (Sandbox Code Playgroud)

程序集输出(带-O标志):

doubleN(unsigned char): # @doubleN(unsigned char)
    add dil, dil
    mov eax, edi
    ret
Run Code Online (Sandbox Code Playgroud)

事实上,这里缺少上面那些"无用的"指令,正如我对优化输出所期望的那样.

Luk*_*odt 18

简短的回答:Godbolt添加了一个-C debuginfo=1标志,强制优化器保持管理帧指针的所有指令.在使用优化进行编译并且没有调试信息时,Rust也会删除这些指令.


那些指示在做什么?

这三条指令是功能序言和结语的一部分.特别是,他们在这里管理所谓的帧指针基指针(rbp在x86_64上).注意:不要将基指针堆栈指针混淆(rsp在x86_64上)!的基指针总是指向当前堆栈帧内:

                          ????????????????????????                         
                          ?  function arguments  ?                      
                          ?         ...          ?   
                          ????????????????????????   
                          ?    return address    ?   
                          ????????????????????????   
              [rbp] ??>   ?       last rbp       ?   
                          ????????????????????????   
                          ?   local variables    ?   
                          ?         ...          ?   
                          ????????????????????????    
Run Code Online (Sandbox Code Playgroud)

关于基指针的有趣之处在于它指向堆栈中的一块内存,它存储了最后一个值rbp.这意味着我们可以很容易地找到前一个堆栈帧的基本指针(来自调用"us"的函数中的一个).

更好的是:所有基本指针形成类似于链表的东西!我们可以轻松地跟随所有人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
Run Code Online (Sandbox Code Playgroud)

这些说明有什么用?

基本指针及其"链表"属性对于调试和分析程序行为(例如分析)非常重要.如果没有基指针,生成堆栈跟踪和定位当前执行的函数会更加困难.

此外,管理帧指针通常不会减慢很多事情.

为什么它们不被优化器删除,我该如何强制执行呢?

如果Godbolt没有传递-C debuginfo=1给编译器,它们通常会是.这指示编译器保留与帧指针处理相关的所有内容,因为我们需要它来进行调试.请注意,调试不一定需要帧指针 - 其他类型的调试信息通常就足够了.存储任何类型的调试信息时都会保留帧指针,因为在Rust程序中删除帧指针仍然存在一些小问题.这个GitHub跟踪问题正在讨论这个问题.

您可以通过自己添加标志-C debuginfo=0来"撤消"它.这导致与C++版本完全相同的输出:

example::double:
    add     dil, dil
    mov     eax, edi
    ret
Run Code Online (Sandbox Code Playgroud)

您还可以通过执行以下命令在本地测试:

$ rustc -O --crate-type=lib --emit asm -C "llvm-args=-x86-asm-syntax=intel" example.rs
Run Code Online (Sandbox Code Playgroud)

如果未明确打开调试信息,则使用optimizations(-O)进行编译会自动删除rbp处理.

  • 好的,那很有道理。只是尚未实现的编译器功能,而不是设计限制。 (2认同)
  • 虽然这对于早期的 Rust 版本来说是准确的,但自 1.27 以来,较新版本的“rustc”中帧指针的省略似乎不再依赖于“debuginfo”:https://godbolt.org/z/j3qcvK9q4 (2认同)