Basic question: `let x = T` where `T` is literal, where value `T` stored?

In other words: let x = [1,2,3,4], where does the value [1,2,3,4] actually allocated?

Based on this simple experiment (playground), correct me if I'm wrong, the expression let x = &[1,2,3,4], the value [1,2,3,4] is embedded (wikipedia). But I'm not sure about where does the value [1,2,3,4] stored if let x = [1,2,3,4].

Also, discovered that binding &[T;N] (size of usize) is potentially faster than that of [T;N] (size of T x N) (playground).

Found a similar post but still did not answer my question. When we discuss where literals are stored, we often refer to &str. But how about non-reference fix-sized values?

Sidenote: I just started learning memory management, std::mem and std::ptr specifically. Everything seems new to me. Please bear with me if I'm asking dumb questions.

It should just be stored on the stack, unless the compiler does some sort of optimization which changes that.

2 Likes

Potentially nowhere. There might just be code to generate it -- same as how let x = 0; doesn't mean there's a zero in-memory waiting to be copied, since it's better to just use an instruction to set x to zero rather that to tell your CPU to copy it from somewhere.

I would say that asking "where" something is when it's just by-value is usually not a useful question.

Unless you take the address of something, talking about an address is mostly unhelpful -- the compiler is incredibly good at optimizing stuff, and by-value things often disappear completely. (As you might notice if you ever try to debug a release build.)

6 Likes

You're right, just tried the release build, no difference between this two. The compiler will take care of all of these.

Of course, in extreme cases, e. g. when the array size is large enough to cause stack overflows, worrying about such differences in debug mode can become relevant again, even if release mode would be optimized properly either way, since you don't want your debug mode run to crash.

2 Likes

This is actually a very good question because answering it touches on a lot of low-level details of how compilers, optimisers, and executables work.

When rustc first generates code machione code for your function, it will (probably) set aside enough space for a [i32; 4][1] variable on the stack (4 * sizeof(i32) = 4 * 4 = 16 bytes) then emit some instructions that write 1_i32 into bit at the start, then a 2_i32 at the spot 4 bytes down, and so on. There are x86 instructions for loading integer constants, so "write 1 to this address" is just one instruciton, then we'd have another instruction for incrementing the pointer by 4, then another instruction for "write 2 to this address", etc.

Later on, any uses of your x variable will use a pointer to the start of the array.

Now this is all well and good, but manually setting aside stack space and initializing the memory with 1, 2, 3, 4 can be slow, especially when your array has lots of elements and not just 4, so the compiler comes with a component called the "optimiser" that will use various tricks to make this code fast.

Depending on the optimiser's mood, there are a couple tricks we could employ...

If the optimiser sees that we only access elements of the array using indices that are known entirely at compile-time, the optimiser can do something called "constant folding" where it'll just copy/paste the array elements into the place it gets used. That means your array might not even exist at runtime!

Another optimisation is to promote the array variable to a constant (sometimes called "static rvalue promotion"). That way, the array's bytes can be hard-coded into your executable and everything referring to that array will be reusing the same thing. This works best when the array will never change (like string literals) and only referred to by reference, but it can also be used in that initialization step from earlier.


  1. I assume i32 here because that's the default type for integer literals. It might be that later code tells the compiler you actually meant the array to be a [usize; 4] or whatever, in which case just mentally replace i32 everywhere I say usize. ↩︎

2 Likes

I think it's important to also somewhat differentiate static rvalue promotion as an optimization from the static rvalue promotion language feature whose RFC you've linked to. The latter is more than just an optimization, because it allows new code to compile, namely code that requires the lifetime of references to such promotable rvalues to be longer than otherwise possible (based on temporary scopes).

This is also the reason why this rvalue promotion will reliably happen in debug mode, too, even when the true optimizations would be turned off. OTOH, if it's important for some reference to get static promotion, then it might be an even better idea to use a const …: &… or maybe even a reference to an actual static …: … variable, because it's more explicit, and the latter (using static) can be useful because it also guarantees[1] that the static value will not be duplicated, which could be relevant for very large values.


  1. maybe that's too strong a term given that all optimizations that don't change observable behavior are allowed; but at least it gives a higher chance that... ↩︎

1 Like

Nah, that's not how it works.

First of all, you should always be benchmarking optimized code. But if the compiler optimizes that piece of code of yours in the Playground, nothing will remain. The whole thing is contained inside a trivial, short function, with the loop bounds being compile-time constants, and the loop body not doing anything externally observable. To an optimizing compiler, that's all noise it will simply throw away with good conscience. You aren't measuring anything meaningful.

Furthermore, you seem to misunderstand what values and addresses and variables are. "Binding a variable" is a concept that only exists in the abstract. It doesn't take CPU cycles to "bind a value to a variable". Values exist in memory, and bindings are names that refer to a specific place in memory. They are a compile-time abstraction; they don't do anything. As such, if you write code that actually need to create an array, then it will create that array equally fast (or slow), regardless of whether you call the entire array x or just its address. (Actually moving a value can cause real memory copying, but the compiler usually optimizes that away, too.)

So basically asking if binding a reference or a value is faster is simply not meaningful. It's like asking whether it is faster to eat a photo of an orange or a photo of a melon. You can't eat either, even though the melon is bigger than the orange.

2 Likes

Yeah, it's hard to be specific without getting into the weeds or over-complicating the answer. The optimisation I'm referring to is effectively static rvalue promotion, except it's done by LLVM and not guaranteed, rather than being something built into the language.

There is a page in the LLVM docs listing the various optimisation passes built into clang.

Some optimisations that look like they'd affect arrays:

3 Likes

I think I understand more now. Whether value/reference binding, it's just a representation at the code level. The actual value is stored in memory regardless thereof.