为什么 Rust 优化器不删除那些无用的指令(在 Godbolt Compiler Explorer 上测试过)?
Why doesn't the Rust optimizer remove those useless instructions (tested on Godbolt Compiler Explorer)?
我想看一下小 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 rbp
、mov rbp, rsp
和 pop rbp
。据我了解,我认为单独执行这三个指令不会产生任何副作用。 那么为什么 Rust 优化器不删除那些无用的指令?
为了对比,我也测试了a C++ version:
unsigned char doubleN(unsigned char n) {
return n + n;
}
汇编输出(带有 -O
标志):
doubleN(unsigned char): # @doubleN(unsigned char)
add dil, dil
mov eax, edi
ret
事实上,这里缺少上面的 "useless" 指令,正如我对优化输出所期望的那样。
简短的回答:Godbolt 添加了一个 -C debuginfo=1
标志,强制优化器保留所有管理帧指针的指令。当使用优化编译且没有调试信息时,Rust 也会删除这些指令。
这些指令在做什么?
这三个指令是function prologue and epilogue. In particular, here they manage the so called frame pointer or base pointer (rbp
on x86_64)的一部分。注意:不要混淆基指针和栈指针(x86_64上的rsp
)! 基指针 总是指向当前栈帧内部:
┌──────────────────────┐
│ function arguments │
│ ... │
├──────────────────────┤
│ return address │
├──────────────────────┤
[rbp] ──> │ last rbp │
├──────────────────────┤
│ local variables │
│ ... │
└──────────────────────┘
基指针的有趣之处在于它指向堆栈中的一块内存,该内存存储rbp
的最后一个值。这意味着我们可以很容易地找到前一个堆栈帧的基指针(来自调用 "us" 的函数的那个)。
更妙的是:所有的基指针都形成了类似于链表的东西!我们可以很容易地跟随所有 last rbp
走上堆栈。这意味着在程序执行期间的每个点,我们都确切地知道哪些函数调用了哪些其他函数,因此我们最终 "here".
让我们再次回顾一下说明:
; 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
这些说明有什么用?
基指针及其 "linked list" 属性 对于一般调试和分析程序行为(例如分析)非常重要。没有基指针,生成堆栈跟踪和定位当前执行的函数会更加困难。
此外,管理帧指针通常不会减慢速度。
为什么优化器没有删除它们?我该如何强制执行?
他们通常会,如果 Godbolt didn't pass -C debuginfo=1
to the compiler. This instructs the compiler to keep all things related to frame pointer handling, because we need it for debugging. Note that frame pointers are not inherently required for debugging -- other kinds of debug info usually suffice. Frame pointers are kept when storing any kind of debug info because there are still a few minor issues related to removing frame pointers in Rust programs. This is being discussed in this GitHub tracking issue。
您只需 adding the flag -C debuginfo=0
yourself 即可 "undo"。这导致与 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
处理。
我想看一下小 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 rbp
、mov rbp, rsp
和 pop rbp
。据我了解,我认为单独执行这三个指令不会产生任何副作用。 那么为什么 Rust 优化器不删除那些无用的指令?
为了对比,我也测试了a C++ version:
unsigned char doubleN(unsigned char n) {
return n + n;
}
汇编输出(带有 -O
标志):
doubleN(unsigned char): # @doubleN(unsigned char)
add dil, dil
mov eax, edi
ret
事实上,这里缺少上面的 "useless" 指令,正如我对优化输出所期望的那样。
简短的回答:Godbolt 添加了一个 -C debuginfo=1
标志,强制优化器保留所有管理帧指针的指令。当使用优化编译且没有调试信息时,Rust 也会删除这些指令。
这些指令在做什么?
这三个指令是function prologue and epilogue. In particular, here they manage the so called frame pointer or base pointer (rbp
on x86_64)的一部分。注意:不要混淆基指针和栈指针(x86_64上的rsp
)! 基指针 总是指向当前栈帧内部:
┌──────────────────────┐
│ function arguments │
│ ... │
├──────────────────────┤
│ return address │
├──────────────────────┤
[rbp] ──> │ last rbp │
├──────────────────────┤
│ local variables │
│ ... │
└──────────────────────┘
基指针的有趣之处在于它指向堆栈中的一块内存,该内存存储rbp
的最后一个值。这意味着我们可以很容易地找到前一个堆栈帧的基指针(来自调用 "us" 的函数的那个)。
更妙的是:所有的基指针都形成了类似于链表的东西!我们可以很容易地跟随所有 last rbp
走上堆栈。这意味着在程序执行期间的每个点,我们都确切地知道哪些函数调用了哪些其他函数,因此我们最终 "here".
让我们再次回顾一下说明:
; 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
这些说明有什么用?
基指针及其 "linked list" 属性 对于一般调试和分析程序行为(例如分析)非常重要。没有基指针,生成堆栈跟踪和定位当前执行的函数会更加困难。
此外,管理帧指针通常不会减慢速度。
为什么优化器没有删除它们?我该如何强制执行?
他们通常会,如果 Godbolt didn't pass -C debuginfo=1
to the compiler. This instructs the compiler to keep all things related to frame pointer handling, because we need it for debugging. Note that frame pointers are not inherently required for debugging -- other kinds of debug info usually suffice. Frame pointers are kept when storing any kind of debug info because there are still a few minor issues related to removing frame pointers in Rust programs. This is being discussed in this GitHub tracking issue。
您只需 adding the flag -C debuginfo=0
yourself 即可 "undo"。这导致与 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
处理。