在Rust中,Option是否编译为运行时检查或指令跳转? [英] In Rust, is Option compiled to a runtime check or an instruction jump?
问题描述
在Rust中,选项
定义为:
pub枚举选项< T> {
无,
Some(T),
}
像这样使用:
fn may_return_none()->选项< i32> {
if is_full_moon {
None
}否则{
Some(1)
}
}
fn main() {
let可选= may_return_none();
match可选{{
None => println!( None),
Some(v)=> println!( Some),
}
}
我不熟悉Rust的内部原理,但是起初我认为它的工作方式类似于.NET中的 Nullable
,所以我上面的Rust代码的编译逻辑如下:
//占用`sizeof(T)+ 1`的存储空间,可能更多,具体取决于`Bool`对齐方式,因此`Nullable< Int32>`占用5个字节。
struct Nullable< T> {
Bool hasValue;
T值;
}
Nullable< Int32> MayReturnNone(){
if(isFullMoon)
//作为结构,Nullable< Int32>通过堆栈返回实例
return Nullable< Int32>(){HasValue = false}
否则
return Nullable< Int32>(){HasValue = true,Value = 1}
}
void Test(){
Nullable< Int32>可选= may_return_none();
if(!optional.HasValue)println( None);
else println( Some);
}
但是,由于所需的空间,这并不是零成本的抽象 Bool hasValue
标志-Rust提出了提供零成本抽象的观点。
我意识到 Option
可以由编译器通过直接返回跳转来实现,尽管它需要将确切的跳转值作为参数提供给堆栈-就像您可以推送多个返回地址:
(伪代码)
mayReturnNone(returnToIfNone,returnToIfHasValue ){
if(isFullMoon){
cleanup-current-stackframe
跳转到returnToIfNone
else {
cleanup-current-stackframe
推入堆栈1
跳转到returnToIfHasValue
}
test(){
mayReturnNone(指令地址(ifHasValue),指令地址(ifNoValue))
ifHasValue:
println( Some)
ifNoVal ue:
println( None)
}
是这样吗实施?这种方法也适用于Rust中的其他 enum
类型-但是,我演示的这个特定应用程序非常脆弱,如果要在调用<之间执行代码,则会中断操作例如,code> mayReturnNone 和 match
语句(如 mayReturnNone
直接跳转到匹配
,跳过中间说明)。
它完全取决于优化。考虑一下此实现(操场):
#![feature(asm)]
外部板条箱兰特;
使用rand :: Rng;
#[inline(never)]
fn is_full_moon()-> bool {
rand :: thread_rng()。gen()
}
fn may_return_none()->选项< i32> {
if is_full_moon(){无}其他{某些(1)}
}
#[inline(never)]
fn use(){
let可选= may_return_none();
匹配可选{{
None =>不安全{asm!( nop)},
Some(v)=>不安全{asm!( nop; nop)},
}
}
fn main(){
usage();
}
在这里,我使用内联汇编而不是打印,因为它没有打印尽可能使结果输出混乱。以下是在发布模式中编译时用法
的程序集:
.section .text._ZN10playground5usage17hc2760d0a512fe6f1E, ax,@ progbits
.p2align 4,0x90
.type _ZN10playground5usage17hc2760d0a512fe6f1E,@ function
usbcage1startc1c1c0c1h b $ b pushq%rax
.Ltmp6:
.cfi_def_cfa_offset 16
callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
testb%al,%al
je .LBB1_2
#APP b $ b nop
#NO_APP
popq%rax
retq
.LBB1_2:
#APP
nop
nop
# NO_APP
popq%rax
retq
.Lfunc_end1:
.size _ZN10playground5usage17hc2760d0a512fe6f1E,.Lfunc_end1-_ZN10playground5usage17hc2760d0a512fe6f1E
> c / pre>
快速精简如下:
- 它调用
is_full _moon
函数( callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
)。
- 测试随机值的结果(
testb%al,%al
)
- 一个分支转到
nop
转到 nop; nop
其他所有内容均已优化。函数 may_return_none
基本上是不存在的。没有创建选项
,从未实现 1
的值。
我确定不同的人有不同的看法,但是我我认为我不能对此进行任何优化。
同样,如果我们使用 Some
中的值(为了方便起见,我将其更改为42):
Some(v)=>不安全{asm!( nop; nop:: r(v))},
然后在使用该值的分支中内联该值:
.section .text._ZN10playground5usage17hc2760d0a512fe6f1E, ax,@ progbits
.p2align 4,0x90
.type _ZN10playground5usage17hc2760d0a512fe6f1E,@ function
_ZN10playground5usage17hc2760d0a512fe6f1E:
.cfi_startproc $ b $ mp_L_t_bfa 16
callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
testb%al,%al
je .LBB1_2
#APP
nop
#NO_APP
popq%rax
retq
.LBB1_2:
movl $ 42,%eax ;;这是
#APP
nop
nop
#NO_APP
popq%rax
retq
.Lfunc_end1:
.size _ZN10playground5usage17hc2760d0a512fe6f1E,.Lfunc_end1-_ZN10playground5usage17hc2760d0a512fe6f1E
.cfi_endproc
$ b $ p>但是,没有任何事情可以围绕契约义务进行优化;如果函数必须返回 Option
,则它必须返回 Option
: #[inline(never)]
pub fn may_return_none()->选项< 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%rat
_b_f_b b $ b callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
movabsq $ 180388626432,%rdx
leaq 1(%rdx),%rcx
testb%al,%al
cmovneq%rdx,%rcx $ b b movq%rcx,%rax
popq%rcx
retq
.Lfunc_end1:
.size _ZN10playground15may_return_none17ha1178226d153ece2E,.Lfunc_end1-_ZN10playground15may_return_none17b1e2e code>
希望我做对了...
- 将64位值0x2A00000000加载到%rdx。 0x2A是42。这是我们正在构建的
Option
;是无
变体。
- 将%rdx +1加载到%rcx中。这是
Some
的变体。
- 我们根据结果测试随机值
- 测试中,是否将无效值移至%rcx或不移至
-
- 将%rcx移至%rax-返回寄存器
这里的要点是,不管优化如何,一个要以特定格式返回数据的函数都必须这样做。仅当与其他代码内联时,才可以删除该抽象。
In Rust, Option
is defined as:
pub enum Option<T> {
None,
Some(T),
}
Used like so:
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"),
}
}
I'm not familiar with Rust internals, but initially I assumed it might work similar to Nullable
in .NET, so the compiled logic of my above Rust code would be like so:
// 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");
}
However this isn't a zero-cost abstraction because of the space required for the Bool hasValue
flag - and Rust makes a point of providing zero-cost abstractions.
I realise that Option
could be implemented via a direct return-jump by the compiler, though it would need the exact jump-to values to be provided as arguments on the stack - as though you can push multiple return addresses:
(Psuedocode)
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")
}
Is this how it's implemented? This approach also works for other enum
types in Rust - but this specific application I've demonstrated is very brittle and breaks if you want to execute code in-between the call to mayReturnNone
and the match
statement, for example (as mayReturnNone
will jump directly to the match
, skipping intermediate instructions).
解决方案 It depends entirely on optimization. Consider this implementation (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();
}
Here, I've used inline assembly instead of printing because it doesn't clutter up the resulting output as much. Here's the assembly for usage
when compiled in release mode:
.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
The quick rundown is:
- It calls the
is_full_moon
function (callq _ZN10playground12is_full_moon17h78e56c4ffd6b7730E
).
- The result of the random value is tested (
testb %al, %al
)
- One branch goes to the
nop
, the other goes to the nop; nop
Everything else has been optimized out. The function may_return_none
basically never exists; no Option
was ever created, the value of 1
was never materialized.
I'm sure that various people have different opinions, but I don't think I could have written this any more optimized.
Likewise, if we use the value in the Some
(which I changed to 42 to find easier):
Some(v) => unsafe { asm!("nop; nop" : : "r"(v)) },
Then the value is inlined in the branch that uses it:
.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
However, nothing can "optimize" around a contractural obligation; if a function has to return an Option
, it has to return an Option
:
#[inline(never)]
pub fn may_return_none() -> Option<i32> {
if is_full_moon() { None } else { Some(42) }
}
This makes some Deep Magic assembly:
.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
Let's hope I get this right...
- Load the 64-bit value 0x2A00000000 to %rdx. 0x2A is 42. This is our
Option
being built; it's the None
variant.
- Load %rdx + 1 into %rcx. This is the
Some
variant.
- We test the random value
- Depending on the result of the test, move the invalid value to %rcx or not
- Move %rcx to %rax - the return register
The main point here is that regardless of optimization, a function that says it's going to return data in a specific format has to do so. Only when it's inlined with other code is it valid to remove that abstraction.
这篇关于在Rust中,Option是否编译为运行时检查或指令跳转?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!