move borrow copy

这一小节没有原创内容,是对这几篇的一个简单描述。

  1. Rust 中的闭包与关键字 move - 知乎
  2. rust 的 move/borrow/copy 语义 - 知乎
  • 不捕获任何环境中变量的闭包可以转换为函数指针。
  • 闭包的方法通过不可变引用访问其捕获的变量的闭包实现了 Fn
  • 闭包的方法通过可变引用访问其捕获变量的闭包实现了 FnMut
  • 闭包的方法若获取了只能被调用一次,即实现了 FnOnce

关键字 move 的作用是将所引用的变量的所有权转移至闭包内,通常用于使闭包的生命周期大于所捕获的变量的原生命周期(例如将闭包返回或移至其他线程)。

编译器倾向于优先通过不可变借用 (immutable borrow) 来捕获闭合变量 (closed-over variable),其次是通过唯一不可变借用 (unique immutable borrow),再其次可变借用 (mutable borrow),最后使用移动语义 (move) 来捕获。编译器将选择这些中的第一个能让此闭包编译通过的选项。这个选择只与闭包表达式的内容有关;编译器不考虑闭包表达式之外的代码,比如所涉及的变量的生存期。

如果使用了关键字 move ,那么所有捕获都是通过移动 (move) 语义进行的

move:乃是所有权从 A 到 B 的交接,进而保障变量只被销毁一次。

borrow:乃是所有权临时从 A 到 B,当 B 完事儿之后送回给 A。借用使用 & 表示。若是借用时可能修改此变量,则使用 &mut 表示。并且内存管理仍然是 A 负责,同样能避免 double free

copy:乃是 B 抄了 A 的值,所有权各执一份,所以即便析构,也是各自析构各自的,同样避免 double free

返回值传值探究

小返回值:寄存器传值

返回值:按理来说,若是返回栈值,则栈帧销毁,返回值失效。就必须从返回堆指针在前一栈帧分配空间中择一。但其实编译器会帮我们处理:

  1. 在栈帧中分配空间
  2. 然后在返回时,将返回值拷贝到寄存器中
  3. 将寄存器拷贝到上层函数的接收处

请看例子:Compiler Explorer

完整代码
fn get_bird() -> Bird {
    Bird { length: 2333, weight: 2333 }
}

example::get_bird:
        pushq   %rax
        movl    $2333, (%rsp)
        movl    $2333, 4(%rsp)
        movl    (%rsp), %eax
        movl    4(%rsp), %edx
        popq    %rcx
        retq

这里将 2333, 2333 送入 rsp 和 rsp + 4 中,然后将 rsp 的值拷贝到 eax 中,将 rsp + 4 的值拷贝到 edx 中。

pub fn main() {
    let bird = get_bird();

example::main:
        subq    $88, %rsp
        callq   example::get_bird
        movl    %edx, 20(%rsp)
        movl    %eax, 16(%rsp)

这里将 edx, eax 拷贝回当前栈帧。完美解决问题。

大返回值:复制到调用者栈帧

那么,如果返回值特别大,两个寄存器放不下呢?

完整代码
fn get_bird() -> Bird {
    Bird {
        length: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
    }
}
example::get_bird:
        subq    $88, %rsp
        movq    %rdi, %rax
        movq    %rax, (%rsp)
        movq    $1, 8(%rsp)
        movq    $2, 16(%rsp)
        movq    $3, 24(%rsp)
        movq    $4, 32(%rsp)
        movq    $5, 40(%rsp)
        movq    $6, 48(%rsp)
        movq    $7, 56(%rsp)
        movq    $8, 64(%rsp)
        movq    $9, 72(%rsp)
        movq    $0, 80(%rsp)
        leaq    8(%rsp), %rsi
        movl    $80, %edx
        callq   memcpy@PLT
        movq    (%rsp), %rax
        addq    $88, %rsp
        retq

有趣,这里可以看到一堆 movq,比如 movq $0, 80 (%rsp),含义是将 0 值拷贝到 rsp + 80,即栈顶的反方向存储。(栈从高位地址往低位地址增长)。

开头的 subq $88, %rsp 表明这些内存都是开配给当前函数 get_bird 的栈帧。

有意思的来了,callq memcpy@PLT,这里相当于将 rsp+8~rsp+80 的内存拷贝到 rdi。

  • @PLT 表示过程链接表,就当是个汇编级别的函数库
  • LLVM 优先按照 rdi、rsi、rdx、rcx 传递参数,见 调用约定 因此 rsi 是第二个参数
  • memcpy 的原型:void *memcpy (void *dest, const void *src, size_t n);

而 rdi 早已在 main 中指定:

example::main:
        subq    $232, %rsp
        leaq    40(%rsp), %rdi
        callq   example::get_bird
        movq    $0, 120(%rsp)

120-40 = 80,这里 80 字节的内存恰好是用来存储 bird 的。

也就是说,如果返回值比较大,实际上会

  1. 提前在调用者栈帧中申请内存。
  2. 当前函数也会在自己的栈帧中申请内存。
  3. 先将结果存放在自己的栈帧中,然后拷贝到调用者的栈帧中。

同样也解决了悬垂指针问题。也不需要申请堆内存造成额外开销。