在Rust中,是否将Option编译为运行时检查或指令跳转?

时间:2017-03-30 23:52:45

标签: compiler-construction rust compiler-optimization

在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标志所需的空间,这不是零成本抽象 - 而Rust提供了零成本抽象的观点。

我意识到Option可以通过编译器的直接返回跳转来实现,尽管它需要在堆栈上作为参数提供精确的跳转值 - 就好像你可以推送多个返回地址:

(伪码)

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和{match的调用之间执行代码就会中断例如{1}}语句(因为mayReturnNone将直接跳转到match,跳过中间指令)。

2 个答案:

答案 0 :(得分:8)

完全取决于优化。考虑这个实现(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
  4. 其他一切都已经过优化。函数may_return_none基本上不存在;没有创建Option1的值从未实现。

    我确信不同的人有不同的意见,但认为我可以写得更优化。

    同样,如果我们使用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    $42, %eax  ;; Here it is
        #APP
        nop
        nop
        #NO_APP
        popq    %rax
        retq
    .Lfunc_end1:
        .size   _ZN10playground5usage17hc2760d0a512fe6f1E, .Lfunc_end1-_ZN10playground5usage17hc2760d0a512fe6f1E
        .cfi_endproc
    

    然而,没有什么能够优化&#34;围绕合同义务;如果某个函数必须返回Option,则必须返回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 $180388626432, %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 - 返回寄存器
    6. 这里的要点是,无论优化如何,一个表示要以特定格式返回数据的功能都必须这样做。只有当它与其他代码一起内联时,删除该抽象才有效。

答案 1 :(得分:2)

警告:这来自调试版本,而不是版本。请参阅另一个答案,了解具有不同行为的优化版本。

您可以查看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    $16, %rsp
.Ltmp9:
    .loc    1 6 0 prologue_end
    callq   is_full_moon@PLT
    movb    %al, -9(%rbp)
    movb    -9(%rbp), %al
    testb   $1, %al
    jne .LBB1_3
    jmp .LBB1_4
.LBB1_3:
    .loc    1 7 0
    movl    $0, -8(%rbp)
    .loc    1 6 0
    jmp .LBB1_5
.LBB1_4:
    .loc    1 10 0
    movl    $1, -8(%rbp)
    movl    $1, -4(%rbp)
.LBB1_5:
    .loc    1 12 0
    movq    -8(%rbp), %rax
    addq    $16, %rsp
    popq    %rbp
    retq
.Ltmp10:
.Lfunc_end1:
    .size   _ZN8rust_out15may_return_none17hb9719b83eae05d85E, .Lfunc_end1-_ZN8rust_out15may_return_none17hb9719b83eae05d85E
    .cfi_endproc

这并没有真正回到不同的地方。 Option<i32>的空格也包含i32值。这意味着您的功能只是编写None/Some标记:

movl    $0, -8(%rbp)

或者也是值:

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

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

  

Rust提出了提供零成本抽象的观点

是一个不适用于每一个案例的假设。