The Tower of Weakenings: Memory Models for Everyone
gankra.github.io
gankra.github.io
Multithreaded--really, multiprocessor--memory models are really, really confusing. Hardware memory models are generally described in terms of what reorderings are possible (e.g., loads may be ordered before subsequent stores), which is easy to understand from a hardware perspective but less easy to deal with from a language perspective. There turns out to be a wonderful theorem, though, that lets you hide all of the hardware ugliness: if you properly synchronize your code, then you can't see any of the reordering, and the compiler merely has to guarantee that the synchronization is properly handled. This is the data-race-free programming model, and it is the primary basis for all modern memory models. Stray away from this model (e.g., relaxed atomics) and you reenter the world of pain, for which no satisfactory solution has yet been found.
Discovering the pains of pointer provenance is even more recent, arguably only about a decade old. For a long time, pointer provenance has been handwaved on a vague data-dependency basis (you see this in LLVM's LangRef, for example). Unfortunately, it turns out that data dependencies are not generally preserved by compilers (this also doomed the release/consume aspect of the C/C++ memory model). An escaping/lifetime-based pointer model is closer to the actual one used by compilers, but there remains tricky things about things like memcpy [1].
It turns out there are two things that make pointer provenance painful: integer-to-pointer (where does the provenance come from?), and memory (since this usually ends up creating implicit integer-to-pointer casts in most putative models that you really want to avoid). The lesson we know from pointer provenance is that pointers are not integers, and cannot be losslessly roundtripped via integers, and Rust being newer and with a heavier emphasis on safety gives it a better chance of adopting a model here that more forcefully breaks this mistaken link.
[1] One consequence is that most formal pointer provenance models actually don't let you legally write memcpy in C code.
It's not funny game. It's part of abstraction hierarchy and physical computing model vs. programming language computing model.
It's only funny because one would presume that there ever was a time the hardware was not playing "funny" game.
And no, it always has a giant abstraction gap between hierarchy, and that's a fundamental part of modern electronic computing. One shall allow oneself to learn those and get used to how things are actually working.
Relaxed atomics are still data-race free, did you mean non-atomic accesses?
In fact, the raison d'être for relaxed atomics is to permit applications to create "benign data races" [in the second sense] that aren't undefined behavior. As it turns out, though, actually specifying what semantics such a "benign data race" has is complicated, especially when you get into the realm of avoiding things like out-of-thin-air behavior (or, to reference a paper that crossed my desk a month ago, undefined behavior executed only if out-of-thin-air happens).
Another is storing pointers in a hash table - you need to do arithmetic on the pointer bits to compute the hash.
The idea is this lets you iterate up and down the linked list, as if you have prev, you can do "neighbours xor prev" to get next, and "neighbours xor next" to get prev.
However, while in that particular program that did create a notable speed improvement and memory saving, I think it's probably worth sacrificing to the greater good.
That's not why Joe Junior's first C program has a linked list in it. But it might well be why Joe Senior's masterpiece Rust program has a linked list in it. On the other hand, depending on the algorithm being implemented, it may only be singly linked, and the XOR trick doesn't apply.
Rust's alloc (the library you get if you have an allocator, but not necessarily the entire OS environment) does provide a linked list if you want one, and this would be a reasonable choice in this case whereas it warns you that you probably wanted Vec if you're not sure which data structure you need.
Or you can go Deep on memory models and try to apply Ralf's Xor Provenance Hack and claim that, well, provenance is stored in bytes, but it doesn't have to be a pointer-sized range of bytes, so let me have some horrible way to express "the high half has provenance 1, the low half has provenance 2" and handwave magic problem solved.
This is of course horrible and also not at all a portable notion to CHERI which tracks provenance at the granularity of "aligned pointer-sized region of memory". But hey, if it helps you sleep at night.
I'm happy to sacrific xoring pointers, the same way I wouldn't use some "cunning trick" to build my house with half as many nails, at the risk that any minor mistake installing any of those nails would lead to my house failing over.
My current Rust project would probably need to hit up the exposed_addr interface, but it's definitely in the twilight of "how to even memory model" since it involves trying to reason about the provenance of pointer values in the register array passed to you by the third argument of the signal handler.
This is super vague half-remembered conversations though.
https://doc.rust-lang.org/core/ptr/fn.read_volatile.html
https://doc.rust-lang.org/core/ptr/fn.write_volatile.html
They're specifically intended for MMIO. "Volatile operations are intended to act on I/O memory, and are guaranteed to not be elided or reordered by the compiler across other volatile operations."
Under the hood, this is issuing the raw load or store as you'd expect, and it shouldn't get touched by the compiler because the load/ store was literally the whole point of the intrinsic rather than a necessary consequence of some higher level language feature like an assignment operation.
Instead it read more like an exasperated rant, that links to rust toolchain improvements. It does have an account of how to use the new features, which was good. But I couldn't glean when and how that would help me as a user
1. Pointers are not addresses; they are pairs of (allocation, address). This is known as pointers having provenance.
2. Treating a pointer as an int-sized address is merely a hardware optimization; and there is already hardware that deliberately does not do this (e.g. CHERI and maybe ARM PAC).
3. The compiler needs to know the allocation part of the pointer (the one that gets lost at runtime) in order to determine if a pointer write will change a local variable. If this association is lost then you get miscompiles.
4. Converting an integer back into a pointer (e.g. usize as ptr) does not establish pointer provenance, will break CHERI, and will miscompile on other architectures.
5. Rust made the mistake of allowing #4 in unsafe code. This is entirely unsound.
6. The proposed strict-provenance APIs allows doing something like #4, but sound, by letting the user stitch an address onto a pointer with a compatible allocation. This re-establishes the chain of provenance and avoids the miscompile.
It's just better if most code doesn't poke the dragon (llvm) and has trivially correct semantics instead of "yeah this has to work but um, don't ask me how or why".
1. Assume the new pointer could be pointing at _anything_ and so disable all optimizations that might possibly be affected. So even trivial things like deducing that "x == y" can be eliminated because x is never modified is right out the window... after all that random integer could somehow have ended up pointing at the memory for x.
2. Assume the new pointer doesn't point at anything the compiler knows about, causing it to make optimizations that break the program. The "x == y" that it eliminated because it hoisted x out of the loop and nothing modified x... turns out your magic pointer from nowhere was pointing at x and now your modifications are never read. Or maybe it decided to put x in a register so it has no memory address. Programmers get really angry when compilers do this.
Provenance is the compiler tracking that you converted ptr -> x, fiddled the bits, then converted x -> ptr2 and being more conservative around optimizations. The Rust change means by default you would only convert a pointer to an integer type right at the point you need to twiddle the bits and only for as long as you need to do so. The compiler then has a better understanding of what you are doing and doesn't need to assume pointers could be pointing at anything.You have to remember the compiler is doing various kinds of inlining, code transformations, etc. Normal things we expect compilers to do. A bunch of small optimizations executed, sometimes iteratively. You can't point to any specific optimization and say "that's the one that breaks things" so you can't just magic this problem away.
Programmers hate compilers that make really slow code (#1), then get really angry when the compiler breaks their program (#2), then get upset about the fix slowing down their code (#1) and the cycle repeats. Often programmers assume compiler writers are just psychopaths who refuse to fix such "obvious" problems. They often pronounce exactly how things should work, demonstrating they're at "step 1" of baby's first memory model.
If fixing this problem with pointers were easy we'd have done it a long time ago.