在 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
快速总结是:
- 它调用
is_full_moon
函数 (callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
)。
- 测试随机值的结果(
testb %al, %al
)
- 一个分支去
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
希望我做对了...
- 加载 64 位值 0x2A00000000 到 %rdx。 0x2A 是 42。这是我们正在构建的
Option
;这是 None
变体。
- 将 %rdx + 1 载入 %rcx。这是
Some
变体。
- 我们测试随机值
- 根据测试结果,是否将无效值移至%rcx
- 将 %rcx 移动到 %rax - return 寄存器
这里的要点是,无论优化如何,一个函数说它要 return 特定格式的数据必须这样做。只有当它与其他代码内联时,删除该抽象才有效。
在 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
快速总结是:
- 它调用
is_full_moon
函数 (callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
)。 - 测试随机值的结果(
testb %al, %al
) - 一个分支去
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
希望我做对了...
- 加载 64 位值 0x2A00000000 到 %rdx。 0x2A 是 42。这是我们正在构建的
Option
;这是None
变体。 - 将 %rdx + 1 载入 %rcx。这是
Some
变体。 - 我们测试随机值
- 根据测试结果,是否将无效值移至%rcx
- 将 %rcx 移动到 %rax - return 寄存器
这里的要点是,无论优化如何,一个函数说它要 return 特定格式的数据必须这样做。只有当它与其他代码内联时,删除该抽象才有效。