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

uqcuzwp8  于 2023-04-21  发布在  其他
关注(0)|答案(3)|浏览(354)

在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类型-但我演示的这个特定应用程序非常脆弱,如果你想在调用mayReturnNonematch语句之间执行代码,就会中断(因为mayReturnNone将直接跳转到match,跳过中间指令)。

llew8vvj

llew8vvj1#

这完全取决于优化。考虑以下实现(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();
}

在这里,我使用了内联汇编而不是打印,因为它不会使结果输出变得混乱。下面是usagerelease模式下编译时的汇编

.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)。
1.测试随机值的结果(testb %al, %al
1.一个分支连接到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    $42, %eax  ;; Here it is
    #APP
    nop
    nop
    #NO_APP
    popq    %rax
    retq
.Lfunc_end1:
    .size   _ZN10playground5usage17hc2760d0a512fe6f1E, .Lfunc_end1-_ZN10playground5usage17hc2760d0a512fe6f1E
    .cfi_endproc

但是,没有什么可以围绕一个契约性义务“优化”;如果一个函数必须返回一个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位值0x 2A 00000000加载到%rdx。0x 2A是42。这是我们正在构建的Option;是None的变种
1.将%rdx + 1加载到%rcx。这是Some变体。
1.我们测试随机值
1.根据测试的结果,是否将无效值移动到%rcx
1.将%rcx移动到%rax -返回寄存器
这里的要点是,不管优化如何,一个函数说它将以特定的格式返回数据,它必须这样做。只有当它与其他代码内联时,才能有效地删除该抽象。

cu6pst1q

cu6pst1q2#

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

你可以在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强调提供零成本的抽象
是一个并不适用于每一个案例的假设

js81xvg6

js81xvg63#

有点晚的党,但我谷歌了同样的事情,我相信这个线程缺乏一些点。
如果给定枚举类型存在无效值,Rust优化器可以省略判别式。这是编译器对所有枚举类型的优化,而不仅仅是Option。像Option<bool>Option<&mut T>这样的东西不会存储额外的标志,但是Option<i32>会,因为i32没有无效值。
在Rust术语中,这种无效的位模式被称为 niche
当然,其他优化也可以应用,正如上面已经提到的那样。编译器可以将常量传播等优化应用于elide Option,但这超出了这个线程的范围。
旁注:这实际上与C程序员所做的非常相似。可选指针通常表示为NULL,但可空的int有点问题,一些函数(如open)选择返回在给定情况下没有意义的值,如-1,但由于明显的原因,这是不可能自动完成的。
参考文献:

相关问题