This is incorrect. It's trivial and compiles just fine. The argument here is that maybe for reasons the programmer doesn't want to - such as not wanting the buffer to outlive its use inside the loop, and they don't want to have to double-nest:
{
let mut buf = [0; 4096];
loop {
...
}
}
That accomplishes exactly the same goal but there's an argument -- not well made in the blog post -- that the compiler should be able to do some form of this hoisting automatically. In C, it would be automatic, because C doesn't make a zero-initialized promise for stack-allocated variables. In Rust it's not because the array is specified as zero-initialized. Of course, C's behavior comes with certain drawbacks of its own. ;)
Rust's behavior isn't unreasonable. It's just a potential missed optimization, but automating it is challenging.
Adding an extra scope here is slightly annoying, but it's not always possible. I think the example in the blog post was poorly chosen, because the complexity of BorrowedBuf together with MaybeUninit doesn't make much sense when your fix makes for much more readable code.
Out of all problems I have encountered with Rust, this is a particularly minor one.
I see. It is a very bad example indeed. Terrible, terrible example.
Switching off Trump mode for a moment, I don't see why you would want to declare the buffer inside the loop, given that keeping it alive for the entire time of the loop is actually the semantics you want.
If people wrote the most optimal code the first time, we wouldn't need optimizing compilers, and all the undefined behavior that optimization passes necessarily bring along. The whole point of the example is to be poorly written in a way that the compiler "obviously" should be able to fix, but can't.
A less obvious example would be...
- A struct, A, which has an init_from_file method that deserializes data from a Read: R
- Another struct, B, which has its own init_from_file, and a variable number of A as one of its fields. B::init_from_file needs to deserialize by calling A::init_from_file in a tight loop.
This example is the same as the first, except now we've disguised the inefficiency with separation of concerns. A compiler can inline A::init_from_file into B::init_from_file to yield the same code as in the example.
So you are saying that the buffer would be allocated in the program in A::init_from_file? And the compiler would be able to optimise that away by allocating the buffer outside the loop?
If the compiler actually does that, that would be a good example. As long as I don't have to be careful to write my code in a way that some obscure compiler optimisation understands.
Because you want the buffer to go out of scope after the last iteration of the loop. Motivating that requires bringing in more rust - It could be as simple as wanting to reuse the variable name later, but more likely it would be because you were using something that had a reference that you wanted to go away so you could borrow it again without the borrow checker yelling at you.
Ok, then the nested scope is indeed exactly what you want. I don't see how obfuscating this purpose and trying to rely on obscure compiler optimisations and intricate semantics would be a good idea.
Yes, that is a valid reason. If I get that optimisation for free, why not?
I still would not rely on that optimisation, though. If I think this could be an actual bottle neck, I would make the shared buffer explicit in my code.
Care to present some proof? Here’s an counter proof that the compiler isn’t able to reason about the memory in that way https://godbolt.org/z/x7j8xoMxY
There are cases where C can do loop hoisting, but the cases are a subset of what Rust does and this isn’t one of those.
You example doesn't show this. Also the allocation is hoisted out of the loop. The initialization is not and this would be invalid in general. It could eliminated in this case, but this would be dead store elimination.
That’s identical to what happens on Rust. The allocation of the underlying uninitialized array is done once (it’s just the stack space allocation) and it assigns the value 0 within the body. It does indeed show exactly that what OP is proposing is something that C isn’t capable of and it’s what the entire article is about - the inability to skip the initialization in the first place by reasoning that it’s not needed.
But wouldn't that change behavior? An empty, zero initialised array will contain data and a bunch of zeroes after a read, but if the next read only partially fills the buffer you end up with a buffer containing data from two reads.
In this specific example there's no issue because the result of read() is being used to only write as much data as was read, but to me this seems like a pretty complicated and unlikely assumption to write optimizations for.
There are some important caveats, though, around trap or non-value representations. Basically, the value held by the storage for a variable may not correspond to a valid value of the variable's type.
For example, a bool variable usually takes a full byte but only has 2 valid representations in many ABIs (0 for false, 1 for true). That leaves 254 trap representations with 8-bit bytes, and trying to read any of these is undefined behavior.
Furthermore, a variable may be stored in a register (unless you take its address with &), and registers can store values wider than the variable type--e.g., even though int has no trap representations in memory of the same size, nowadays it's usually smaller than a register--or be in a state that makes them unreadable. Trying to read such a value is also undefined behavior.
So, reading memory in general is defined behavior (just with an indeterminate value) but it has to actually be memory and you have to be reading it into a type that can accept arbitrary bit patterns.