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

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.据我所知,我认为仅执行这三条指令没有任何副作用.So why doesn't the Rust optimizer remove those useless instructions?


为了进行比较,我还测试了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

事实上,上面那些"无用"的指令丢失了,正如我从优化的输出中所期望的那样.

推荐答案

简单的回答是:Godbolt adds a 100 flag which forces the optimizer to keep all instructions managing the frame pointer.在使用优化和没有调试信息的情况下编译时,Rust也会删除这些指令.


这些说明在做什么?

这三条指令是function prologue and epilogue条指令的一部分.尤其是在这里,他们管理着所谓的frame pointer or base pointer (rbp on x86_64).注意:不要混淆base pointerstack pointer(x86_64上的rsp)!base pointer始终指向当前堆栈帧内:

                          ┌──────────────────────┐                         
                          │  function arguments  │                      
                          │         ...          │   
                          ├──────────────────────┤   
                          │    return address    │   
                          ├──────────────────────┤   
              [rbp] ──>   │       last rbp       │   
                          ├──────────────────────┤   
                          │   local variables    │   
                          │         ...          │   
                          └──────────────────────┘    

关于基指针的有趣之处在于,它指向堆栈中存储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

这些说明有什么用?

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

此外,管理帧指针通常不会降低很多速度.

为什么它们没有被优化器删除,我如何执行它?

他们通常是,如果Godbolt didn't pass -C debuginfo=1 to the compiler岁的话.这指示编译器保留与帧指针处理相关的所有内容,因为我们需要它进行调试.请注意,调试并不需要帧指针——其他类型的调试信息通常就足够了.在存储任何类型的调试信息时都会保留框架指针,因为在Rust程序中删除框架指针仍然存在一些小问题.这将在this GitHub tracking issue中讨论.

你只需要adding the flag -C debuginfo=0 yourself次就可以"撤销"它.这将产生与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相关问答推荐

如何在 struct 中填充缓冲区并同时显示它?

什么是Rust惯用的方式来使特征向量具有单个向量项的别名?

Rust:跨多个线程使用hashmap Arc和rwlock

为什么这是&q;,而让&q;循环是无限循环?

用 rust 蚀中的future 展望 struct 的future

像这样的铁 rust 图案除了‘选项’之外,还有其他 Select 吗?

由于生存期原因,返回引用的闭包未编译

期望一个具有固定大小 x 元素的数组,找到一个具有 y 元素的数组

具有多个键的 HashMap

为什么 js_sys Promise::new 需要 FnMut?

Rust Option 的空显式泛型参数

Rust ECDH 不会产生与 NodeJS/Javascript 和 C 实现相同的共享密钥

在没有任何同步的情况下以非原子方式更新由宽松原子操作 Select 的值是否安全?

返回优化后的标题:返回异步块的闭包的类型擦除

使用 Rust 从 Raspberry Pi Pico 上的 SPI 读取值

在运行时在 Rust 中加载字体

无法把握借来的价值不够长寿,请解释

为什么 match 语句对引用类型比函数参数更挑剔?

Rust 中的通用 From 实现

类型组的通用枚举