move总是复制数据吗?

问题描述 投票:0回答:1

假设我们有一个函数将相对较大的仅堆栈数据传递给另一个函数,如下所示:

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?

注意:我知道我们可以保证数组的数据不会通过使用引用/切片来复制,但这不是这个问题的目的。

rust compilation llvm
1个回答
0
投票

让我们修改一下您的示例以简化一下:

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

在汇编代码中可以看到几个部分:

  1. 创建数组(直到
    call memset
    行),这只是将 1024 字节初始化为 42。
  2. 获取数组的第 123 个元素并显示它(直到
    call std::io::stdio::_print
    )。
  3. 将数组地址移动到
    rdi
    (按照惯例,用于传递典型 Linux amd64 ABI 中函数的第一个参数)
  4. 致电
    fn b()
  5. 再次执行步骤 2-3,这次是在
    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];

我已经在这里回答了类似的问题,我的答案包括了一些本答案中未提及的细节。

© www.soinside.com 2019 - 2024. All rights reserved.