Stream: cranelift

Topic: Register allocation


view this post on Zulip ender (Mar 30 2021 at 00:26):

If you wrote let x = ManuallyDrop::new(String::from("Hello World!")); in a Rust crate, would liveness analysis by the compiler allocate a register to x for the entire program?

view this post on Zulip Chris Fallin (Mar 30 2021 at 03:59):

@ender good question! The short answer is "no" -- it's not necessary to keep a value in a register just because at the programming language level an object (like a string) is alive.

The longer answer is: there are several levels of abstraction to understand, corresponding to the steps in compilation. At the Rust semantics level, the Rust compiler reasons about how long objects have to live, and where to drop them (if at all). At some point in the rustc pipeline, drops turn into explicit function calls. I assume (I don't know how ManuallyDrop is implemented exactly) that if the effect of the line of code you wrote is that the string is never dropped, that means that, at the IR level, we just have: (i) call String::from; (ii) do nothing for the rest of the program. In other words, if the String is not actually used again (by a normal use or by a drop), then the data within it is not actually live.

A key principle in compilers that may be useful to understand this is "observational equivalence". What that means is that it's legal for the compiler to transform the code in any way that keeps the observable behavior the same. If the observable behavior is simply -- String is allocated, String is never dropped (ie nothing else happens) -- then it's legal to turn this into a program that calls String::from and promptly forgets everything about the returned information. The memory just leaks, and we don't need to keep the pointer around to do that!

Finally, at the IR-to-machine-code level, it should be noted that if a value is actually alive for a long span within a function, that doesn't mean it consistently lives in a register. It will often be "spilled" to a storage slot in the stack frame if the register is needed for some other purpose. Also, if the pointer is in some function and that function calls other functions, it will be saved on the stack (either by the caller or the callee, depending on the ABI and which register). The really important bit that falls out of this is that register allocation is local to each function. So in other words, we never think about things in terms of "allocate rax for the whole program" (for example); rather, we always think in terms of live ranges within a single function.

Hope that clears things up!


Last updated: Oct 23 2024 at 20:03 UTC