为什么Rust在LLVM IR闭包环境中将闭包捕获的i64存储为i64*?

tib*_*bbe 3 rust

在这个简单的例子中

#[inline(never)]
fn apply<F, A, B>(f: F, x: A) -> B
    where F: FnOnce(A) -> B {
    f(x)
}

fn main() {
    let y: i64 = 1;
    let z: i64 = 2;
    let f = |x: i64| x + y + z;
    print!("{}", apply(f, 42));
}
Run Code Online (Sandbox Code Playgroud)

传递给的闭包apply作为LLVM IR传递{i64*, i64*}*:

%closure = type { i64*, i64* }
define internal fastcc i64 @apply(%closure* noalias nocapture readonly dereferenceable(16)) unnamed_addr #0 personality i32 (i32, i32, i64, %"8.unwind::libunwind::_Unwind_Exception"*, %"8.unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality {
entry-block:
  %1 = getelementptr inbounds %closure, %closure* %0, i64 0, i32 1
  %2 = getelementptr inbounds %closure, %closure* %0, i64 0, i32 0
  %3 = load i64*, i64** %2, align 8
  %4 = load i64*, i64** %1, align 8
  %.idx.val.val.i = load i64, i64* %3, align 8, !noalias !1
  %.idx1.val.val.i = load i64, i64* %4, align 8, !noalias !1
  %5 = add i64 %.idx.val.val.i, 42
  %6 = add i64 %5, %.idx1.val.val.i
  ret i64 %6
}
Run Code Online (Sandbox Code Playgroud)

(apply实际上在生成的LLVM代码中有一个更复杂的名称.)

这导致两个负载到达每个捕获的变量.为什么不%closure只是{i64, i64}(这会引起争论apply {i64, i64}*)?

Fra*_*gné 6

默认情况下,闭包通过引用捕获.您可以通过move在参数列表之前添加关键字来更改该行为以按值捕获:

let f = move |x: i64| x + y + z;
Run Code Online (Sandbox Code Playgroud)

这会生成更精简的代码:

define internal fastcc i64 @apply(i64 %.0.0.val, i64 %.0.1.val) unnamed_addr #0 personality i32 (i32, i32, i64, %"8.unwind::libunwind::_Unwind_Exception"*, %"8.unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality {
entry-block:
  %0 = add i64 %.0.0.val, 42
  %1 = add i64 %0, %.0.1.val
  ret i64 %1
}
Run Code Online (Sandbox Code Playgroud)

添加move关键字意味着闭包使用的任何值都将移动到闭包的环境中.在整数的情况下Copy,它没有太大的区别,但在其他类型的情况下String,这意味着String在创建闭包后不能再在外部范围中使用它.这是一个全有或全无的交易,但你可以手动接受move闭包之外的各个变量,并让闭包使用这些引用而不是原始值来获得手动捕获引用行为.

您能否在此代码中以某种方式观察值与ref的差异?

如果您获取捕获变量的地址,您可以观察到差异.注意第一和第二输出线是如何相同的,第三条是不同的.