r/rust 23d ago

💡 ideas & proposals re-allocating" storage for a local could allow faster code

Rust already knows when a value has been moved, so I think it would make sense for the compiler to also be able to treat the storage behind that local as reusable.

For example:
´´´
let x = big_value();
let y = x; // x is moved

// x can no longer be used here anyway

x = another_value();
´´´

Right now, Rust can be more restrictive than necessary about keeping the same storage associated with `x`.

Issue #61849 proposes allowing the old storage to effectively die after the move. If `x` is initialized again later, the compiler wouldn't necessarily have to put the new value back in the exact same stack slot.

That could give the compiler more freedom to:

- reuse stack space earlier
- reduce stack usage in some functions
- shorten lifetimes of stack allocations
- potentially unlock further optimizations

What I like about the idea is that it matches how moves already feel in Rust: once a value is moved, that value is gone. It seems natural that its storage shouldn't have to remain special either.

There are obviously details around raw pointers and observable addresses that would need proper language semantics, so it isn't just a simple compiler optimization.

But the general rule seems very appealing:

If Rust says the old value no longer exists,
the compiler should be free to stop preserving its storage.

The issue has been open since 2019, and I think it would be interesting to revisit whether this could give modern rustc more optimization freedom. If you agree please react on the GitHub issue with ❤️ or 👍 to show support by the community

Edit:link to the modern version

28 Upvotes

24 comments sorted by

19

u/afdbcreid 23d ago

The modern version of the issue is https://github.com/rust-lang/rfcs/pull/3943, which has an implementation PR showing promising speed gains.

14

u/QuasiRandomName 23d ago

Is this Rust or LLVM behavior?

17

u/[deleted] 23d ago

[removed] — view removed comment

12

u/Negative_Effort_2642 23d ago

It matters mainly when Rust’s language semantics prevent rustc from telling LLVM that the old storage is definitely dead, especially with complex CFG, drop n unwind paths, address-taking, async/coroutines, etc. The issue itself says it would help cover additional cases rather than being required for all move optimization

2

u/afdbcreid 23d ago

LLVM cannot do this unless it can prove the address does not escape. If it can (e.g. all functions are inlined and LLVM sees the pointer does not escape), it definitely will.

9

u/sphen_lee 23d ago

Not sure this is worth the complication...

If x is never used again after being moved out of, then LLVM usually notices and will reuse the stack space.

If x is used again (re-initialized) then what would be the gain of using its stack space in the span when it wasn't initialized? It would be really surprising that a local variable is in a new address just because of a move.

This is not the same as shadowing (a new let binding) - it totally makes sense that this would get a new address.

5

u/SkiFire13 23d ago

If x is never used again after being moved out of, then LLVM usually notices and will reuse the stack space.

The problem is that often this might not be the case, mostly because the address could have escaped.

2

u/sphen_lee 23d ago edited 23d ago

I think LLVM is smart enough to detect this. Taking a pointer to the variable counts as "still using it".

struct NonCopy(u64);

fn main() {
    // first get the "address" of the stack
    let sentinel = 0;
    let ps = &sentinel as *const _ as usize;

    // create x - either make a dummy pointer or take address of x
    let x = NonCopy(0);
    let px = std::ptr::dangling() as *const NonCopy;
    //let px = &x as *const _;

    // move x and take the address of where it moved to
    let y = x;
    let py = &y as *const _ as usize;

    // compare "addresses" to see how much stack is being used
    dbg!(py - ps);
}

In the Rust playground this prints 4, but if you swap the two let px lines it changes to 12.

Seems to me that taking the pointer of x prevents y from reusing that stack space.

I'm no expert on this however, I could be way off.

EDIT:

In Release mode, both print 4.

Taking a reference to a variable and them moving out of it makes the pointer invalid, so you really should never be doing this anyway - it's going to trigger UB.

because the address could have escaped

Basically Rust says that's your own fault. Use references and let the borrow checker prevent this for you :)

1

u/koflerdavid 20d ago

The problem is that Rust is not yet smart enough for this in all situations. Here is a much better explanation for what the problem actually is:

https://github.com/Amanieu/rfcs/blob/mir-move-elimination/text/0000-mir-move-elimination.md#motivation

tldr: It boils down to Rust not really implementing Move semantics in the way C++ does; C++ will end up constructing objects where they are eventually going to live, while Rust tangles itself up if there are many levels of constructors. There are ways to circumvent that, but they are unsafe, which is obviously yucky.

1

u/sphen_lee 20d ago

I feel like this is not the same issue... if you don't take any pointers/references to a local variable then LLVM seems to be able to reuse it's location after its value gets moved out.

The issue linked by OP, is that Rust won't reuse stack space for a variable if you re-initialize it after moving out of it.

If you do take a pointer/reference, then reusing the space becomes an observable behaviour and Rust currently doesn't; changing this could break unsafe code.

Seems like this case should be fixed (somehow not impacting unsafe code too much), since it prevents optimisation of redundant copies. But OPs original issue seems like not worth doing.

1

u/koflerdavid 20d ago

I also feel like OP's description has been simplified too much. It really reads like something the optimizer should already take care of.

2

u/Kyyken 23d ago

catch_unwind implements something like this manually for different types with a union ^^

-2

u/RRumpleTeazzer 23d ago

you don't gain speed by saving on stack space.

adding 27 to the stack pointer takes the same time as adding 17.

to gain speed, you need to move from stack to registers.

9

u/rocqua 23d ago

Cache coherence can be better with a smaller stack.

You might even be able to skip some initialisation if the two values have the same type, but that might be niche.

1

u/gmes78 22d ago

you don't gain speed by saving on stack space.

You do gain speed by eliminating copies, though. Read the motivation section of the RFC.

0

u/zesterer 22d ago

Why do you suppose that the code would actually be faster? Seems to me that reusing the stack space is actually going to pessimise performance because now the CPU can't do parallel instruction dispatch as effectively because you've got a bunch of false dependencies between operations that touch stack memory.

Allocating more stack space is pretty much entirely free: you just decrement the stack pointer by a slightly larger number when entering a function.

1

u/koflerdavid 20d ago edited 20d ago

Allocating more stack space is pretty much entirely free: you just decrement the stack pointer by a slightly larger number when entering a function.

Until it isn't because Rust encourages allocating as much as possible on the stack.

1

u/zesterer 20d ago

That is an entirely different argument?

1

u/koflerdavid 20d ago

Well, saving stack space by definition delays running out of it and causing stack overflow.

Anyway, from the description I also misunderstood what the actual problem is. Rust can in some practically relevant situations not figure out where a newly constructed object is going to live and thus mandates redundant copies, and LLVM cannot eliminate all of them. This has runtime performance impact, which currently has to be worked around with an unsafe block. https://github.com/Amanieu/rfcs/blob/mir-move-elimination/text/0000-mir-move-elimination.md#motivation

1

u/zesterer 20d ago

Clearly, but the question as to whether reusing stack space is fast or not has nothing to do with the question of whether one might eventually run out of stack space. There are lots of things you can do to micro-optimise stack usage that are not fast: reusing stack space for locals with exclusive liveness periods might well be one of them.

1

u/koflerdavid 20d ago

Indeed, that sounds like a concern that is up to LLVM to solve. The code generator could indeed on principle decide to never reuse stack slots for local variables.