假设我们有一个函数将相对较大的仅堆栈数据传递给另一个函数,如下所示:
fn a() {
let arr_a: [i32; 1024] = [1, 2, 3, ...];
b(arr_a);
}
fn b(arr_b: [i32; 1024]) {
// ... do stuff with arr_b here
}
用 Rust 术语来说,当
b
被调用时,a
的 arr_a
将被 移动 到 b
的 arr_b
。在幕后,整个数组always是否会被复制到堆栈上,或者编译器是否有可能通过简单地使用arr_a
的数据,在内存地址处进行优化,而不复制它?如果是后者,编译器的哪一部分应该负责? LLVM?
注意:我知道我们可以保证数组的数据不会通过使用引用/切片来复制,但这不是这个问题的目的。
让我们修改一下您的示例以简化一下:
pub fn main() {
let mut arr_a: [u8; 1024] = [42; 1024];
let val = arr_a[123];
println!("{}", val);
b(arr_a);
}
#[inline(never)]
fn b(arr_b: [u8; 1024]) {
let val = arr_b[123];
println!("{}", val);
}
这会编译成以下汇编代码(假设 amd64 架构和
-C opt-level=2
):
example::main::hd2bfa2df25bfe7d7:
push rbx
sub rsp, 1104
lea rbx, [rsp + 80]
mov edx, 1024
mov rdi, rbx
mov esi, 42
call qword ptr [rip + memset@GOTPCREL]
mov byte ptr [rsp + 15], 42
lea rax, [rsp + 15]
mov qword ptr [rsp + 16], rax
mov rax, qword ptr [rip + core::fmt::num::imp::<impl core::fmt::Display for u8>::fmt::ha81407c30cb780ca@GOTPCREL]
mov qword ptr [rsp + 24], rax
lea rax, [rip + .L__unnamed_1]
mov qword ptr [rsp + 32], rax
mov qword ptr [rsp + 40], 2
mov qword ptr [rsp + 64], 0
lea rax, [rsp + 16]
mov qword ptr [rsp + 48], rax
mov qword ptr [rsp + 56], 1
lea rdi, [rsp + 32]
call qword ptr [rip + std::io::stdio::_print::hd6837e34a66547dd@GOTPCREL]
mov rdi, rbx
call example::b::hea8802b300eb5620
add rsp, 1104
pop rbx
ret
example::b::hea8802b300eb5620:
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
在汇编代码中可以看到几个部分:
call memset
行),这只是将 1024 字节初始化为 42。call std::io::stdio::_print
)。rdi
(按照惯例,用于传递典型 Linux amd64 ABI 中函数的第一个参数)fn b()
fn b()
的体内注意这里不涉及复制;编译器足够聪明,可以看到您不需要该值的副本,因此它只是将指针传递给已经存在的数组。但是,请记住,某些操作(例如打印出变量的内存地址,或者甚至将其直接传递到
println!()
而不是先创建局部变量)可能会改变此行为。
关于问题的
which part of the compiler should be responsible for that
部分 - 您可以看到在 Rust MIR 级别上完成的优化(也可以在编译器资源管理器中看到),因此它是由 Rust 编译器完成的,而不是 LLVM:
铁锈米尔:
fn main() -> () {
let mut _0: ();
let mut _1: [u8; 1024];
// ...
bb1: {
StorageDead(_4);
StorageDead(_6);
_9 = b(move _1) -> [return: bb2, unwind continue];
}
// ...
}
fn b(_1: [u8; 1024]) -> () {
debug arr_b => _1;
let mut _0: ();
let _2: u8;
let _3: ();
// ...
_9 = b(move _1) -> [return: bb2, unwind continue];
部分告诉我们编译器只会将指针传递给已经存在的数组。相反的是 b(copy _1)
,我们会有一个 memcpy
。为了进行比较,以下代码在调用 b()
之前生成一个数组副本,因为我们试图查看值的内存地址:
pub fn main() {
let mut arr_a: [u8; 1024] = [42; 1024];
println!("{:p}", &arr_a);
b(arr_a);
}
#[inline(never)]
fn b(arr_b: [u8; 1024]) {
println!("{:p}", &arr_b);
}
事实上,我们可以在 Rust MIR 代码中看到以下行:
_9 = b(copy _1) -> [return: bb2, unwind continue];
我已经在这里回答了类似的问题,我的答案包括了一些本答案中未提及的细节。