2

My intuition must be wrong about moves and copies. I would expect the Rust compiler optimize away moves of an immutable value as a no-op. Since the value is immutable, we can safely reuse it after the move. But Rust 1.65.0 on Godbolt compiles to assembly that copies the value to a new position in memory. The Rust code that I am studying:

pub fn f_int() {
    let x = 3;
    let y = x;
    println!("{}, {}", x, y);
}

The resulting assembly with -C opt-level=3:

    ; pub fn f_int() {
    sub     rsp, 88
    ; let x = 3;
    mov     dword ptr [rsp], 3
    ; let y = x;
    mov     dword ptr [rsp + 4], 3
    mov     rax, rsp
    ...

Why does let y = x; result in mov dword ptr [rsp + 4], 3 and mov rax, rsp? Why doesn't the compiler treat y as the same variable as x in the assembly?

(This question looks similar but it is about strings which are not Copy. My question is about integers which are Copy. It looks like what I am describing is not a missed optimization opportunity but a fundamental mistake in my understanding.)

Student
  • 127
  • 1
  • 6

2 Answers2

4

If you change your example like this

pub fn f_int() -> i32 {
    let x = 3;
    let y = x;
    // println!("{}, {}", x, y);
    x+y
}

the optimisation takes place

example::f_int:
        mov     eax, 6
        ret

The println!() macro (as well as write!()...) takes references on its parameters and provides the formatting machinery with these references. Probably, the compiler deduces that providing some functions (that are not inlined) with references requires the data being stored somewhere in memory in order to have an address. Because the type is Copy, the semantics implies that we have two distinct storages, otherwise, sharing the storage would have been an optimisation for a move operation (not a copy).

prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • That’s interesting about `x+y`! Why is it a problem to pass on two identical memory addresses to such a function? Copy would imply that they are separate values, but due to immutability the compiler could safely assume that they are the same and store only one copy in memory. What would it break if the compiler did this? – Student Feb 18 '23 at 18:20
4

I would not call it a fundamental mistake in your understanding, but there are some interesting observations here.

First, println!() (and the formatting machinery in particular) is surprisingly hard to optimize, due to its design. So the fact that with println!() it was not optimized is not surprising.

Second, it is generally not obvious it is OK to perform this optimization, because it observably make the addresses equivalent. And println!() takes the address of the printed values (and passes them to an opaque function). In fact, Copy types are harder to justify than non-Copy types in that regard, because with Copy types the original variable may still be used after a move while with non-Copy types it is possible that not.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • So is the `mov` performed in the assembly because the memory addresses are required to be different? What use case would it break if the addresses were allowed to be the same? I am confused because I would expect immutability to imply the reuse of the original memory address on a copy, as I think e.g. Haskell does. – Student Feb 18 '23 at 18:14
  • @Student People expect that different allocations have different addresses. This is the point of e.g. `std::ptr::ptr_eq()` and people rely on that. – Chayim Friedman Feb 19 '23 at 10:16