True, it would be an observable difference in behavior if (for example) the function compares the addresses of arguments between invocations (thus preventing general optimization). I'm not quite sure, does Rust guarantee anything about the addresses (for example stored as a usize
) after the end of the lifetime? At that point there is no guarantee about anything you'd read from that memory address (could segfault), but the address itself? Consider the following:
fn main() {
let a = T::new();
let b = T::new();
outer(a);
outer_2(b);
}
#[inline(never)]
fn outer(c: T) {
do_stuff(&c);
}
#[inline(never)]
fn outer_2(d: T) {
// Random variable that isn't used and can be optimized away
let x = u64;
do_stuff(&d);
}
By the time outer_2 is called, a
has been dropped (could also have been moved somewhere else by outer
, but let's ignore that for now). do_stuff
could store the address somewhere and it would be valid, save Rust (as long as there is no memory access using that address). If the compiler removes x
(To be fair: I'm not 100% sure if it actually does that) or for some reason changes the stack layout of outer_2
so it is different to that of outer
(for example when enabling optimizations and completely eliminating a redundant stack variable), or any other change that affects the layout on the stack it would change if b
is or is not at the same address as a
. Therefore any comparison between the address c
and d
are pointing to is already completely up to the compiler/optimizer/LLVM as to what the result will be (as a
and b
are moved into the function).
The same goes the other way: Is there anything (guarantees given by the compiler) that prevents the Rust compiler from moving k
around between calls to do_stuff
in the following example? It is not Pin
and thus not guaranteed not to move. (Disclaimer: I don't know if the compiler is allowed to add moves when it thinks it is useful. Probably not, as this is effectively the same as this problem: An optimization or change with an observable difference in behavior).
fn main() {
let k = T::new();
// I'm using mutable references in this example, so it's clear that
// this only applies when the lifetime "ends" when `do_stuff` exits
// and `do_stuff` can only keep the address (without any guarantees
// about the underlying memory).
do_stuff(&mut k);
// Do a bunch of other stuff, allocations, function calls, whatever
// Call it again
do_stuff(&mut k);
}
I'd argue that relying on the address of something being different or equal to a later reference, which was created after the end of the lifetime of the previous reference is fundamentally unstable and (likely) not useful except perhaps for some kind of heuristic. That future versions of the compiler could at any time change something in the stack layout or how functions are called which would break any promises about the value of an address (not the value of the thing at that address) that may exist, hence I conclude that there are no such guarantees (at least across compiler versions and most likely across optimizer levels/settings).
So yes, when just considering one single optimization step only using lifetime analysis (instead of function body analysis) could result in a difference in observable behavior, but there are a lot of other things (especially across compiler versions) with a similar impact (basically any observation of stack memory addresses depend on this). Hence my question of what guarantees even exist about the address (again, not the memory location) after the end of the lifetime you got that address from.
(again, it's quite possible that I'm missing something here or that this would be considered a breaking change due to it not being documented anywhere)
I wouldn't be surprised if nobody depends on an address (no memory access) after the end of a lifetime, due to this.
Long story short: I'm curious what you think about this and if there are any such guarantees (e.g. across compiler versions)