Fix bugs with remote entity allocation#24972
Conversation
This uses relaxed atomics, which prevents subtle data races in remote allocation.
| // SAFETY: Caller ensures memory ordering. | ||
| // The `Slot` has the same memory representation as `Entity` | ||
| // and currently represents a valid entity value because this is not concurrent with any `free`. | ||
| unsafe { core::ptr::from_ref(target).cast::<Entity>().read() } |
There was a problem hiding this comment.
Why have a special case here? It adds a lot of complexity, and a non-atomic load isn't any faster than a Relaxed load.
If we do need a special case here, then I'd prefer to avoid relying on the implementation details of Entity. This could be cast::<u64> followed by from_bits, which should compile down to the same thing, but would only rely on the layout of Slot and not the layout and interpretation of Entity and all of the wrapper types it's built on.
(This would also remove the need for #[repr(align(8))].)
There was a problem hiding this comment.
For now, I've kept it and changed this to use from_bits. That's a great idea.
Why have a special case here? It adds a lot of complexity, and a non-atomic load isn't any faster than a
Relaxedload.
Agreed on the complexity, but I mean, it's not so bad, especially if it's ok to remove the 32-bit atomics shenanigans. Plus, non-atomic ops could be faster here in some cases. I'm no asm expert, but it might be possible that FreeBufferIterator could see some optimizations from not needing any atomics.
In short, I didn't do this because I thought it would improve performance. I did it because I didn't know that not doing it wouldn't hurt performance. But if anyone feels strongly about this, I suppose it can be removed. It certainly doesn't affect x86. IDK about arm.
There was a problem hiding this comment.
In short, I didn't do this because I thought it would improve performance. I did it because I didn't know that not doing it wouldn't hurt performance. But if anyone feels strongly about this, I suppose it can be removed. It certainly doesn't affect x86. IDK about arm.
I won't block on it, but I would recommend removing it. This is tricky unsafe code, so we should try to make it as clear and readable as possible, and only add complexity when necessary for performance. Having a const generic parameter and several extra safety requirements to check is quite a bit of complexity for something that we know won't affect x86 and have no reason to believe will affect other architectures.
Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com> Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
Objective
Fixes #24897.
I thought I could be clever here, but @Imberflur, who apparently reads random atomics code in their free time (which is awesome,) discovered some UB issues in the new entity allocator from #18670. I believe I've fixed those issues in this PR, but I also believed there weren't any to begin with, so that's not exactly authoritative. Now, the last time this code was reviewed, 11 people reviewed it (12 including myself) and none of use saw this, so while this should truly fix everything, it definitely deserves some eagle-eyed reviewing!
In the future, we could consider doing some form of
loomorshuttletesting here, but that's not for this PR, and there are good reasons to not do this too. (loomcan be extraordinarily slow for tests andshuttleis non-deterministic by nature, IIRC.)Solution
See #24897 for the details on the initial bugs, but we basically need to use relaxed atomics for slots and add acquire/release ordering for how
remote_allocinteracts withfree.Additionally, I made the
Slottype have identical memory layout and bit validity asEntity. That means non-remotealloccan skip the atomics because it already has strict ordering withfree.The only really tricky part is handling
Sloton platforms that don't have 64-bit atomics. We could do nothing and let portable atomics do the work, but that could be a tad slow on some platforms because we don't need true atomics. That is, we don't care if theu64we get back is "whole and correct" or not; we just need it to not race. I've opted to split it into 32-bit atomics on those platforms. That adds significant complexity though, so if we can accept slightly worse performance on those platforms, it would be nice to just useAtomicU64everywhere. IDK what the real-world performance difference would be, but I don't have a way to benchmark it either.Testing
All tests pass, etc, but they did last time, too, so that doesn't mean much with these fixes. It might also be nice to test this on a platform without 64-bit atomics, but I can't do that locally and don't know if CI does it either.
I have not benchmarked this for two reasons. First, on most platforms, like x86, this should be functionally the same as it was before, and second, I don't think it can be any faster without re-introducing UB or a new allocator paradigm (and I don't have a v10 hidden up my sleeves).