tibbe
tibbe

Reputation: 9009

Why does Rust store i64s captured by a closure as i64*s in the LLVM IR closure environment?

In this simple example

#[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));
}

the closure passed to apply is passed as a 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
}

(apply actually has a more complicated name in the generated LLVM code.)

This causes two loads to get to each of the captured variables. Why isn't %closure just {i64, i64} (which would make the argument to apply {i64, i64}*)?

Upvotes: 3

Views: 182

Answers (1)

Francis Gagn&#233;
Francis Gagn&#233;

Reputation: 65937

Closures capture by reference by default. You can change that behavior to capture by value by adding the move keyword before the parameter list:

let f = move |x: i64| x + y + z;

This generates much leaner code:

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
}

Adding the move keyword means that any value that the closure uses will be moved into the closure's environment. In the case of integers, which are Copy, it doesn't make much difference, but in the case of other types like String, it means that you can't use the String anymore in the outer scope after creating the closure. It's an all-or-nothing deal, but you can manually take references to individual variables outside a move closure and have the closure use these references instead of the original values to get manual capture-by-reference behavior.

Can you observe the value vs ref difference somehow in this code?

If you take the address of the captured variable, you can observe the difference. Notice how the first and second output lines are the same, and the third is different.

Upvotes: 6

Related Questions