在 Rust 中,Option 被编译成运行时检查还是指令跳转?

In Rust, is Option compiled to a runtime check or an instruction jump?

在 Rust 中,Option 定义为:

pub enum Option<T> {
    None,
    Some(T),
}

这样使用:

fn may_return_none() -> Option<i32> {
    if is_full_moon {
        None
    } else {
        Some(1)
    }
}

fn main() {
    let optional = may_return_none();
    match optional {
        None => println!("None"),
        Some(v) => println!("Some"),
    }
}

我不熟悉 Rust 内部结构,但最初我认为它可能与 .NET 中的 Nullable 类似,所以我上面的 Rust 代码的编译逻辑如下:

// occupies `sizeof(T) + 1` memory space, possibly more depending on `Bool`'s alignment, so `Nullable<Int32>` consumes 5 bytes.
struct Nullable<T> {
    Bool hasValue;
    T value;
}

Nullable<Int32> MayReturnNone() {
    if( isFullMoon )
        // as a `struct`, the Nullable<Int32> instance is returned via the stack
        return Nullable<Int32>() { HasValue = false }
    else
        return Nullable<Int32>() { HasValue = true, Value = 1 }
}

void Test() {
    Nullable<Int32> optional = may_return_none();
    if( !optional.HasValue ) println("None");
    else                     println("Some");
}

然而,这不是零成本抽象,因为 Bool hasValue 标志需要 space - 而 Rust 强调提供零成本抽象。

我意识到 Option 可以通过编译器的直接 return-jump 来实现,尽管它需要将确切的跳转到值作为堆栈上的参数提供 - 如尽管您可以推送多个 return 地址:

(伪代码)

mayReturnNone(returnToIfNone, returnToIfHasValue) {

    if( isFullMoon ) {
        cleanup-current-stackframe
        jump-to returnToIfNone
    else {
        cleanup-current-stackframe
        push-stack 1
        jump-to returnToIfHasValue
    }

test() {

    mayReturnNone( instructionAddressOf( ifHasValue ), instructionAddressOf( ifNoValue ) )
ifHasValue:
    println("Some")
ifNoValue:
    println("None")
}

是这样实现的吗?这种方法也适用于 Rust 中的其他 enum 类型——但我演示的这个特定应用程序非常脆弱,如果你想在调用 mayReturnNone 和 [=20] 之间执行代码,它会中断=] 语句,例如(因为 mayReturnNone 将直接跳转到 match,跳过中间指令)。

警告:这来自调试版本,而不是发布版本。请参阅其他答案以了解行为不同的优化版本。

您可以查看 Rust playground

上的代码

函数编译为:

    .cfi_startproc
    pushq   %rbp
.Ltmp6:
    .cfi_def_cfa_offset 16
.Ltmp7:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
.Ltmp8:
    .cfi_def_cfa_register %rbp
    subq    , %rsp
.Ltmp9:
    .loc    1 6 0 prologue_end
    callq   is_full_moon@PLT
    movb    %al, -9(%rbp)
    movb    -9(%rbp), %al
    testb   , %al
    jne .LBB1_3
    jmp .LBB1_4
.LBB1_3:
    .loc    1 7 0
    movl    [=10=], -8(%rbp)
    .loc    1 6 0
    jmp .LBB1_5
.LBB1_4:
    .loc    1 10 0
    movl    , -8(%rbp)
    movl    , -4(%rbp)
.LBB1_5:
    .loc    1 12 0
    movq    -8(%rbp), %rax
    addq    , %rsp
    popq    %rbp
    retq
.Ltmp10:
.Lfunc_end1:
    .size   _ZN8rust_out15may_return_none17hb9719b83eae05d85E, .Lfunc_end1-_ZN8rust_out15may_return_none17hb9719b83eae05d85E
    .cfi_endproc

这并不是真的回到不同的地方。 Option<i32> 的 space 也包含 i32 值。这意味着您的函数正在编写 None/Some 标记:

movl    [=11=], -8(%rbp)

或者值也是:

movl    , -8(%rbp)
movl    , -4(%rbp)

所以我猜你的问题的答案是:

Rust makes a point of providing zero-cost abstractions

是一个假设,并不适用于所有情况。

全靠优化。考虑这个实现 (playground):

#![feature(asm)]

extern crate rand;

use rand::Rng;

#[inline(never)]
fn is_full_moon() -> bool {
    rand::thread_rng().gen()
}

fn may_return_none() -> Option<i32> {
    if is_full_moon() { None } else { Some(1) }
}

#[inline(never)]
fn usage() {
    let optional = may_return_none();
    match optional {
        None => unsafe { asm!("nop") },
        Some(v) => unsafe { asm!("nop; nop") },
    }
}

fn main() {
    usage();
}

在这里,我使用了内联汇编而不是打印,因为它不会使结果输出混乱太多。这是 usage 发布模式 下编译时的程序集:

    .section    .text._ZN10playground5usage17hc2760d0a512fe6f1E,"ax",@progbits
    .p2align    4, 0x90
    .type   _ZN10playground5usage17hc2760d0a512fe6f1E,@function
_ZN10playground5usage17hc2760d0a512fe6f1E:
    .cfi_startproc
    pushq   %rax
.Ltmp6:
    .cfi_def_cfa_offset 16
    callq   _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
    testb   %al, %al
    je  .LBB1_2
    #APP
    nop
    #NO_APP
    popq    %rax
    retq
.LBB1_2:
    #APP
    nop
    nop
    #NO_APP
    popq    %rax
    retq
.Lfunc_end1:
    .size   _ZN10playground5usage17hc2760d0a512fe6f1E, .Lfunc_end1-_ZN10playground5usage17hc2760d0a512fe6f1E
    .cfi_endproc

快速总结是:

  1. 它调用 is_full_moon 函数 (callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E)。
  2. 测试随机值的结果(testb %al, %al)
  3. 一个分支去 nop,另一个去 nop; nop

其他一切都已优化。函数 may_return_none 基本上不存在; Option 从未被创建,1 的价值从未被实现。

我敢肯定不同的人有不同的意见,但是认为我不能再优化这个了。


同样,如果我们使用 Some 中的值(我改为 42 以便更容易找到):

Some(v) => unsafe { asm!("nop; nop" : : "r"(v)) },

然后在使用它的分支中内联该值:

    .section    .text._ZN10playground5usage17hc2760d0a512fe6f1E,"ax",@progbits
    .p2align    4, 0x90
    .type   _ZN10playground5usage17hc2760d0a512fe6f1E,@function
_ZN10playground5usage17hc2760d0a512fe6f1E:
    .cfi_startproc
    pushq   %rax
.Ltmp6:
    .cfi_def_cfa_offset 16
    callq   _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
    testb   %al, %al
    je  .LBB1_2
    #APP
    nop
    #NO_APP
    popq    %rax
    retq
.LBB1_2:
    movl    , %eax  ;; Here it is
    #APP
    nop
    nop
    #NO_APP
    popq    %rax
    retq
.Lfunc_end1:
    .size   _ZN10playground5usage17hc2760d0a512fe6f1E, .Lfunc_end1-_ZN10playground5usage17hc2760d0a512fe6f1E
    .cfi_endproc

然而,没有什么可以"optimize"绕过合同义务;如果一个函数必须return一个Option它必须return一个Option:

#[inline(never)]
pub fn may_return_none() -> Option<i32> {
    if is_full_moon() { None } else { Some(42) }
}

这使得一些 Deep Magic 组件:

    .section    .text._ZN10playground15may_return_none17ha1178226d153ece2E,"ax",@progbits
    .p2align    4, 0x90
    .type   _ZN10playground15may_return_none17ha1178226d153ece2E,@function
_ZN10playground15may_return_none17ha1178226d153ece2E:
    .cfi_startproc
    pushq   %rax
.Ltmp6:
    .cfi_def_cfa_offset 16
    callq   _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
    movabsq 0388626432, %rdx
    leaq    1(%rdx), %rcx
    testb   %al, %al
    cmovneq %rdx, %rcx
    movq    %rcx, %rax
    popq    %rcx
    retq
.Lfunc_end1:
    .size   _ZN10playground15may_return_none17ha1178226d153ece2E, .Lfunc_end1-_ZN10playground15may_return_none17ha1178226d153ece2E
    .cfi_endproc

希望我做对了...

  1. 加载 64 位值 0x2A00000000 到 %rdx。 0x2A 是 42。这是我们正在构建的 Option;这是 None 变体。
  2. 将 %rdx + 1 载入 %rcx。这是 Some 变体。
  3. 我们测试随机值
  4. 根据测试结果,是否将无效值移至%rcx
  5. 将 %rcx 移动到 %rax - return 寄存器

这里的要点是,无论优化如何,一个函数说它要 return 特定格式的数据必须这样做。只有当它与其他代码内联时,删除该抽象才有效。