考虑片段
struct Foo {
dummy: [u8; 65536],
}
fn bar(foo: Foo) {
println!("{:p}", &foo)
}
fn main() {
let o = Foo { dummy: [42u8; 65536] };
println!("{:p}", &o);
bar(o);
}
该计划的典型结果是
0x7fffc1239890
0x7fffc1229890
地址不同。
显然,大数组
dummy
已被复制,正如编译器移动实现中所预期的那样。不幸的是,这可能会对性能产生不小的影响,因为 dummy
是一个非常大的数组。这种影响可能会迫使人们选择通过引用传递参数,即使函数实际上在概念上“消耗”了参数。
由于
Foo
不导出 Copy
,因此对象 o
被移动。由于 Rust 禁止访问移动的对象,那么是什么阻止 bar
“重用”原始对象 o
,迫使编译器生成可能昂贵的按位副本?是否存在根本性的困难,或者我们有一天会看到编译器优化掉这个按位复制?
鉴于在 Rust 中(与 C 或 C++ 不同),值的地址不被认为是重要的,因此就语言而言,没有任何东西可以阻止副本的省略。
然而,今天 rustc 没有优化任何东西:所有优化都委托给 LLVM,看来你在这里遇到了 LLVM 优化器的限制(目前还不清楚这个限制是由于 LLVM 接近 C 的语义还是只是一个遗漏) ).
因此,有两种改进代码生成的方法:
但现在您可能只是想避免在堆栈上分配如此大的对象,您可以
Box
例如。
实际上没有什么可以阻止编译器像这样优化数组副本。问题在于,将值传递给像
println!
这样的东西似乎非常谨慎,因为它们实际上总是收到对这些值的引用,并且可能由于某种原因依赖于这些值的确切地址。这正是您的示例中的情况:您甚至明确要求提供地址,因此编译器足够聪明,不会优化这些地址 - 毕竟您要求的是副本。
让我们看一下您问题中稍微简化的示例:
fn bar(foo: [u8; 65536]) {
println!("{}", foo[123]);
}
pub fn main() {
let o = [42u8; 65536];
println!("{}", o[123]);
bar(o);
}
您可以使用 -C opt-level=2
通过
Compiler Explorer运行它,并看到以下汇编代码:
example::main::hd2bfa2df25bfe7d7:
push r15
push r14
push r12
push rbx
mov r11, rsp
sub r11, 131072
.LBB0_1:
sub rsp, 4096
mov qword ptr [rsp], 0
cmp rsp, r11
jne .LBB0_1
sub rsp, 72
lea rbx, [rsp + 65608]
mov edx, 65536
mov rdi, rbx
mov esi, 42
call qword ptr [rip + memset@GOTPCREL]
lea rax, [rsp + 65731]
mov qword ptr [rsp + 8], rax
mov r14, qword ptr [rip + core::fmt::num::imp::<impl core::fmt::Display for u8>::fmt::ha81407c30cb780ca@GOTPCREL]
mov qword ptr [rsp + 16], r14
lea r15, [rip + .L__unnamed_1]
mov qword ptr [rsp + 72], r15
mov qword ptr [rsp + 80], 2
mov qword ptr [rsp + 104], 0
lea rax, [rsp + 8]
mov qword ptr [rsp + 88], rax
mov qword ptr [rsp + 96], 1
mov r12, qword ptr [rip + std::io::stdio::_print::hd6837e34a66547dd@GOTPCREL]
lea rdi, [rsp + 72]
call r12
lea rdi, [rsp + 72]
mov edx, 65536
mov rsi, rbx
call qword ptr [rip + memcpy@GOTPCREL]
lea rax, [rsp + 195]
mov qword ptr [rsp + 56], rax
mov qword ptr [rsp + 64], r14
mov qword ptr [rsp + 8], r15
mov qword ptr [rsp + 16], 2
mov qword ptr [rsp + 40], 0
lea rax, [rsp + 56]
mov qword ptr [rsp + 24], rax
mov qword ptr [rsp + 32], 1
lea rdi, [rsp + 8]
call r12
add rsp, 131144
pop rbx
pop r12
pop r14
pop r15
ret
大部分代码在这里并不真正相关,但重要的是它调用
memset
(创建一个 65536 字节长的数组,所有值都设置为 42),然后在调用 memcpy
之前调用 bar()
。如果你仔细观察,你会发现 bar
甚至已经被内联在这里,但数组仍在被复制。
现在让我们再次更改示例,以消除在
println!
语句中对数组的任何直接使用:
fn bar(foo: [u8; 65536]) {
let val = foo[123];
println!("{}", val);
}
pub fn main() {
let o = [42u8; 65536];
let val = o[123];
println!("{}", val);
bar(o);
}
现在汇编代码看起来完全不同了:
example::main::hd2bfa2df25bfe7d7:
push r15
push r14
push r12
push rbx
sub rsp, 72
mov byte ptr [rsp + 6], 42
lea rax, [rsp + 6]
mov qword ptr [rsp + 8], rax
mov rbx, qword ptr [rip + core::fmt::num::imp::<impl core::fmt::Display for u8>::fmt::ha81407c30cb780ca@GOTPCREL]
mov qword ptr [rsp + 16], rbx
lea r14, [rip + .L__unnamed_1]
mov qword ptr [rsp + 24], r14
mov qword ptr [rsp + 32], 2
mov qword ptr [rsp + 56], 0
lea r15, [rsp + 8]
mov qword ptr [rsp + 40], r15
mov qword ptr [rsp + 48], 1
mov r12, qword ptr [rip + std::io::stdio::_print::hd6837e34a66547dd@GOTPCREL]
lea rdi, [rsp + 24]
call r12
mov byte ptr [rsp + 7], 42
lea rax, [rsp + 7]
mov qword ptr [rsp + 8], rax
mov qword ptr [rsp + 16], rbx
mov qword ptr [rsp + 24], r14
mov qword ptr [rsp + 32], 2
mov qword ptr [rsp + 56], 0
mov qword ptr [rsp + 40], r15
mov qword ptr [rsp + 48], 1
lea rdi, [rsp + 24]
call r12
add rsp, 72
pop rbx
pop r12
pop r14
pop r15
ret
没有
memset
、memcpy
或其他大块内存操作的痕迹。这些值已在编译时计算并直接传递到 std::io::stdio::_print()
的两次调用中。当然,这个例子过于简单了,但重点是编译器可以优化这些东西。
最后一个例子:
#[inline(never)]
fn bar(foo: [u8; 65536]) {
let val = foo[123];
println!("{}", val);
}
pub fn main() {
let o = [42u8; 65536];
bar(o);
}
注意
#[inline(never)]
处的fn bar()
!这会编译成以下汇编代码:
example::bar::hd9b3b9e4606cd696:
sub rsp, 72
movzx eax, byte ptr [rdi + 123]
mov byte ptr [rsp + 7], al
lea rax, [rsp + 7]
mov qword ptr [rsp + 8], rax
mov rax, qword ptr [rip + core::fmt::num::imp::<impl core::fmt::Display for u8>::fmt::ha81407c30cb780ca@GOTPCREL]
mov qword ptr [rsp + 16], rax
lea rax, [rip + .L__unnamed_1]
mov qword ptr [rsp + 24], rax
mov qword ptr [rsp + 32], 2
mov qword ptr [rsp + 56], 0
lea rax, [rsp + 8]
mov qword ptr [rsp + 40], rax
mov qword ptr [rsp + 48], 1
lea rdi, [rsp + 24]
call qword ptr [rip + std::io::stdio::_print::hd6837e34a66547dd@GOTPCREL]
add rsp, 72
ret
example::main::hd2bfa2df25bfe7d7:
push rbx
mov r11, rsp
sub r11, 65536
.LBB1_1:
sub rsp, 4096
mov qword ptr [rsp], 0
cmp rsp, r11
jne .LBB1_1
mov rbx, rsp
mov edx, 65536
mov rdi, rbx
mov esi, 42
call qword ptr [rip + memset@GOTPCREL]
mov rdi, rbx
call example::bar::hd9b3b9e4606cd696
add rsp, 65536
pop rbx
ret
在这里您可以看到,即使
main
函数创建了实际的数组,它也没有被复制到 bar()
中。相反,它的地址只是保存在 rbx
寄存器中,然后作为 rdi
中的指针传递给 bar
函数。零拷贝,我们都好!