-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MaybeUninit::uninit(); N] memsets memory #96274
Comments
Looks like this literally does a Surprised this is not optimized ... but I'm afraid that adding this optimization now would not be accepted, because it is technically illegal, due to the poison/undef distinction (https://alive2.llvm.org/ce/z/-Uryfw). |
It's not what we want, yeah. But LLVM is way outside my purview, all I can say is that LLVM is allowed to not memset them -- in that sense, they are equivalent. "Equivalent" doesn't mean "generates the same assembly code" (that would be basically impossible to guarantee), it means "has the same observable behavior and the same UB". @nikic this is new memory though, not memory that could even contain |
Oh wait, that's a different code than in the OP? I was talking about |
Though not being able to optimize away "overwriting with undef" is also exceedingly silly. That's an LLVM design flaw. I hope they still plan to fix this by killing Could we work around this by overwriting with |
FWIW I put up https://reviews.llvm.org/D124173 to implement the optimization, but I don't know if it will be accepted. It would be silly not to accept it given that we do the same thing for plain stores, but sometimes people get pedantic in weird places :)
I believe the undef here comes from a load from uninitialized memory -- there are plans to switch that to produce poison (llvm/llvm-project#52930), but those are long term plans that will involve a good bit of work. |
Currently it behaves like this: let mut p = [MaybeUninit::<u8>::uninit(); 571]; // memset
let mut p: [MaybeUninit<u8>; 571] = MaybeUninit::uninit_array(); // no memset
let mut p = MaybeUninit::<[u8; 571]>::uninit(); // no memset |
Maybe this is a stupid question, but why is rust emitting a memset(undef) in the first place? That seems like a rust bug rather than an llvm bug. Question about LLVM: @nikic isn't your PR actually invalid? That would depend on undef propagation, i.e. does passing an undef through a parameter require it to materialize or does the undef propagate through allowing every use to take on a different value? If it can take on a different value, then your change is fine. Otherwise, that optimization is invalid because every byte under the memset must be identical (even if it's some random value). Going back to this begin a rust bug, here's what I mean. Let's compare this to an unconditional branch optimization. Presumably rust emits ir for an |
@SUPERCILEX In
I don't believe we specify the behavior of memset with undef in that much detail, but generally speaking undef does not need to be materialized at call boundaries, and can take on different values for each use in the called function. Of course, things are a bit tricky when it comes to intrinsics, as we don't have a function implementation to consult in that case. |
Thanks for all these details! I think I still don't understand uninitialized memory then. 😅 To me, "copies uninitialized memory" is a nonsensical statement — how can you copy something that doesn't exist? Conceptually, I've been thinking about uninitialized memory from the abstract machine perspective as the presence of a box in which to put stuff. You can put undefined stuff in there no problem, but you have to put something in there for it to not be a void (aka uninitialized). Is that just totally wrong? I guess I'm coming at this from the C angle in which uninitialized memory is just a fat pointer. |
Think of uninit memory like this: every byte of memory either holds an initialized enum AbstractByte {
Init(u8),
Uninit,
} We can just copy a list of See this blog post for some more details. |
Mmmm, I like that enum, thanks. So then the uninit variant is being mapped to llvm's undef? In that case,
This issue should stay open to keep an eye on https://reviews.llvm.org/D124173 while the rest of the discussion can be moved to the uninit_array tracking issue. Does that sound like a plan? |
Yeah, basically.
No they don't? Both create N bytes of memory filled with |
Except isn't the first one copying a bunch of small types filled with |
There's no difference between how those two things are represented as a sequence of The
|
Good to know, thanks.
That's the critical distinction I'm referencing. I agree with you that there's no difference from the sequence-of-AbstractBytes perspective, but one version involves a copy whereas the other does not which makes a huge difference from the practical what-I-want-to-happen perspective. The whole point of having a buffer of MaybeUninits is to avoid unnecessary writes to memory. Whether or not the copies from Does that make sense? I feel like I'm not getting through. :) |
I think I see what you mean, but I disagree. The following two functions are contextually equivalent, which means they are as equivalent as things can get in a programming language: it is impossible to tell the difference between them in terms of program behavior; one can be replaced by the other without program behavior changing even in the slightest. fn mk_uninit1<N: const usize>() -> [MaybeUninit<u8>; N] {
[MaybeUninit::uninit(); N]
}
fn mk_uninit2<N: const usize>() -> [MaybeUninit<u8>; N] {
MaybeUninit::uninit_array()
} But program equivalence is undecidable (think: halting problem), so it can easily happen that the optimizer treats one different from the other. When that happens, we should fix the optimizer, not design our libs APIs around it. To try an analogy: you should write |
Upstream fix landed in llvm/llvm-project@1881711. |
Woohoo! @RalfJung I've been mulling this one over and I think the fundamental disagreement centers around
From the performance perspective, this is wrong. To touch on your analogy, the goal of |
In Rust we never guarantee the performance of anything, at least not in the same sense that we guarantee that Sometimes it is too hard for the compiler to achieve the desired performance all by itself, so we design new APIs that are harder to use but easier to turn into high-performance implementations. But I don't think I am even fundamentally disagreeing with you. ;) I just think that new APIs for better performance should be a last resort if we can't get the compiler to do the right thing by itself for all relevant cases -- our opinions only really differ in this threshold. |
Ok, I think I'm finally convinced. 😁 My one remaining worry now would be around regression testing. Is there some way we can know if the LLVM side of things breaks? And then should |
We have LLVM codegen tests for this, yes ( |
I created #98304 to ensure there's no memset, but it still fails. How can one track the progress of an LLVM diff into rust's version of LLVM? Basically when can I expect that test to pass? |
We'll update to LLVM 15 once it branches, I'd expect it to land in August. |
Sounds good, I'll keep an eye out for LLVM 15. |
Add MaybeUninit memset test Closes rust-lang#96274
I tried this code (from #93668 (comment)):
Godbolts: https://godbolt.org/z/341fEjqMd, https://godbolt.org/z/K9dMx8Whr
I expected to see this happen: no memset.
Instead, this happened: uninitialized memory is not actually uninitialized.
The docs explicitly say that these two should be equivalent: https://doc.rust-lang.org/stable/std/mem/union.MaybeUninit.html#method.uninit_array
The text was updated successfully, but these errors were encountered: