What's up with this new memory_order_consume memory order?
devblogs.microsoft.com
devblogs.microsoft.com
For a while the ISO documents claimed Consume was "Temporarily discouraged" and I believe now they just admit you'll get Acquire instead. Because Rust follows the C++ 11 Model (as revised) for lack of anything better, it deliberately does not mention Consume ordering either. https://doc.rust-lang.org/core/sync/atomic/enum.Ordering.htm...
consume was not invented for Alpha. rather consume should have less overhead than acquire on any platform that needs additional memory barriers to implement acquire semantics (on most platforms consume requires no barriers at all since memory ordering is enforced as a side-effect of dependency ordering. Alpha is an exception to this, i.e. consume does require a barrier on Alpha).
it's also worth noting that rcu_dereference in linux is based on the same idea as consume (using dependencies to enforce memory ordering) the the basic idea is definitely useful. it's more that the C++ standard's specification of consume turned out to be unimplementable in practice
Um... which platform?[1] Probably the biggest reason that memory ordering semantics are such a mess, and lockless algorithms such an infeasible disaster, is the propensity of people to argue about this stuff in generic terms, using language and concepts from egghead standards writers instead of examples of real machines. Out of order execution is an engineering technique for physical devices. We should talk about the machines first, and the abstractions later.
It's like going to the mechanic with a leaky valve and having the explanation come back in terms of cycle efficiencies and isentropic losses. It's not actually helpful to understanding the problem unless you already understand the theory. And you won't ever learn the theory unless you see the problem it's solving.
[1] It's a serious question, btw. I genuinely don't know what the target hardware is for "consume" myself, and suspect this is just a pet theory that snuck into the standard.
What's part of it is a mess?
I'm also not sure I understand your point about physical machines. Surely what we care about are the guarantees each platform gives.
There are plenty of cases where the documented worst case is worse than you'll ever be able to replicate - for now. Sticking to the contract means you're safe on next year's machines, too.
To be more serious: anyone who's ever tried to implement a lockless algorithm knows how hard this stuff is even if you have a clear and unambiguous set of tools to use to do it. And the C++ memory model is, as mentioned, kinda ambiguous.
At the end of the day, in practice these problems are solved on real hardware with real tools that look like "MFENCE" or "DSB" (either directly, or because you're reading the generated assembly trying to figure out how it's going wrong). That's exactly the opposite of the way a good abstraction is supposed to work.
As far as lockless algorithms go - using something like Loom for Rust greatly simplifies correctness. It has a relatively complete implementation of the C++ memory model, and will catch bizarre edge cases you'd struggle to ever replicate on a real CPU. There are also solvers for C++ that help you do similar reasoning.
For _performance_, on the other hand, it's the wild west. I experimented with pairing my home grown system similar to Loom with a MESI cache simulator, testing cache behaviour with lockless algorithms banging on the same cache lines. It showed some promise, but was quite difficult to make ergonomic for the consumer.
This seems like semantic evasion? Consume exists[1] precisely so that you can write code using the C++ memory model that works correctly on a superscalar Alpha chip (not the 21064/21164, those are in-order). Saying that it doesn't have anything to do with alpha seems really strained. If alpha didn't exist then consume wouldn't exist and people would be writing these as unconstrained/normal loads because that works everywhere else.
[1] Again, AFAIK. Lots of folks in this thread, including you, seem to be implying that other such hardware exists. But I'm not aware of it. And my broader point is that this kind of "standards first" discussion obscures understanding.
Still, in practice because of a mixture of being hard to implement[1] and x86 dominating compiler development, most compilers gave up and just implemented consume as an acquire.
[1] to make consume work, compilers need to make sure that data dependencies are propagated everywhere they are needed and this is very hard.
edit: also in-order machines are very much capable of the kind of reordering that require memory barriers. IIRC the lack of data dependency of alpha was a quirk due to its banked cache setup, not because of pipeline reordering. In fact data-dependences is specifically the kind of reordering that an OoO engine can't do.
No? Those algorithms can be implemented cheaply already on every architecture BUT Alpha! You just don't use a barrier there, because none is needed. The only reason we're even talking about consume is becasue alpha exists and requires a barrier instruction on dependent loads. No alpha, no consume. QED.
Again I repeat: the attempt to obscure this and pretend that features like "consume" are first class theoretic entities unconnected to hardware is IMHO really, really hurting people's ability to understand this subject. It's not like that, it's a hack. Call it a hack to support a legacy architecture and then people will understand why it's there. Pretend that all possible[1] designs for ordering synchronization would require "consume" and you'll have everyone tied up in arguments like this trying to understand what it's for.
[1] Which is impossible, obviously. I do much of my day job on an architecture with an incoherent cache where NONE of these silly toys work to solve ordering issues, because they're aimed at a different layer of the stack.
edit: to be more explicit, you can't formally implement RCU with relaxed + a compiler barrier, although in practice it might work most of the time.
No, you have it wrong. Consume exists so that you can write code that works correctly on all chips other than Alpha, x86, and SPARC.
> If alpha didn't exist then consume wouldn't exist and people would be writing these as unconstrained/normal loads because that works everywhere else.
That's patently false. For the semantics to work correctly, you still need to identify the memory operations that generate synchronization edges. Unannotated loads, by definition, can't generate those edges. Relaxed atomics are sufficient from a hardware perspective, but it ignores the effect of the compiler. The problem with consume--why it's never worked--is that it relies on the compiler preserving data dependencies expressed in the source language, which runs into the issue of a) defining what exactly constitutes a data dependence (hardware implementations don't exactly agree!) and b) this turns out to be a lot more difficult to do than originally anticipated.
To even have a hope of being able to correctly reason about the need to not have a load barrier in these places, you need a big, flashing neon sign to the compiler saying "this is special with special semantics, please pay attention." That sign is memory_order_consume.
Consume does not AFAICT act as a memory clobber or optimization barrier to the compiler.
I.e. the quirks of Alpha have no bearing on the existence of consume. A different annotation between relaxed and acquire would still needed to specifically capture the semantics of data dependencies (that's the theory at least, in practice even consume and the [[carries_dependency]] annotations are not enough).
This is your confusion. It is precisely an optimization barrier. Specifically, it's an acquire barrier, but only for certain data-dependent subsequent loads. Compilers universally treat it as a global acquire barrier because it turns out that it's too difficult to ensure that language-level data dependencies translate to hardware-level data dependencies.
On x86, the hardware never needs acquire barriers, so the relaxation to an acquire barrier has no effect. However, although the hardware doesn't need acquire barriers, the compiler still needs to know where those exist in the program so that it doesn't reorder the loads anyways. Meanwhile, on Alpha, the hardware still needs an acquire barrier in the case where there's a data dependency. So in both the ultra-weak and the rather strong memory models, translating memory_order_consume as memory_order_acquire has no actual difference to the hardware outcomes.
This is why memory_order_consume is really for ARM et al--the compiler still needs to know about the existence of the barrier, but the hardware doesn't need the barrier. And this is why there's still efforts going on to fix memory_order_consume; the people who work on ARM and the like want to get to the point where they can write these codes without having the compiler emit acquire barriers.
I don't understand what that means. Compiler barriers are about changing the ordering of instructions. "DEpendent" loads have defined ordering already, you can't issue a load before loading the pointer register you're dereferencing!
I have to ask again: can you give me an example from any toolchain and any architecture other than Alpha where a consume generates a detectable difference in the generated code?
if (i == 3) {
y = x[i];
}
The compiler will happily replace this code with: if (i == 3) {
y = x[3];
}
Now, the load of `x[i]` is no longer data-dependent on `i`. This is a real optimization that happens in compilers, and the Linux kernel (which relies on something akin to release/consume, although not directly dependent on the C11 memory model) has documentation specifically warning against using the results of the consume-like load in comparisons precisely because this optimization exists and has caused problems in the past.This is also why compilers have generally ignored memory_order_consume, by-the-by. Supporting consume 'properly' (i.e., other than strengthening it into a memory_order_acquire) requires suppressing optimizations that do not preserve data dependencies, but consume leaks out too much to the point that it's basically not possible to prove that the dependency isn't part of a consume chain, and the optimization is too beneficial to be worth turning off entirely.
(Edit: it's also true that this is a regime where we've seen security bugs due to optimization exactly like this. And that's true. But that's also not a memory ordering issue and it's, again, not resolvable by use of the C++ consume load feature).
So we're back to "consume exists so you can write C++ code in 2023 to run on an architecture Compaq cancelled in 2003". Right?
[1] Though it's true that my second phrasing was more ambiguous than my first.
C is not some thin translation layer for assembly, nor has it been for my entire lifetime (and I really wish people stopped teaching C as if it were merely "portable assembly"). At best, it is a description of a machine that does not, and will never, exist in practice, and the job of the compiler can be viewed as trying to emulate that machine using existing hardware. (This is still a somewhat poor description, but it does the job a lot better.) Trying to use hardware to understand how C works can be a fools' errand, because it's approaching the problem backwards. We don't try to describe hardware in C, rather, we try to think how C can be efficiently implemented--emulated--in hardware.
This comes to a very clear head when it comes to memory models. The abstract machine envisioned by C and C++ is, fundamentally, a sequentially consistent memory model. Now, no multiprocessor implements a sequentially consistent memory model, it's just not performant. The reason we can get away with such an unimplementable memory model detail is because of the data-race-free property: if you have release and acquire operations that obey certain properties, and your program is free of inter-thread dependencies that don't go through these operations (a data race, by definition) [1], then even weak memory ordering implementations are observationally indistinguishable from sequentially consistent implementations.
Because of the data-race-free model, the release and acquire barriers are fundamental to understanding the correctness of code. If they don't exist, then the code is definitionally wrong. And since the data-race-free model is nearly as old as I am, it's very well understood what hardware instructions need to be added to implement these barriers on every major platform. On some architectures, notably x86, the realization of these barriers is to do absolutely nothing; the hardware of loads and stores is sufficient to provide the guarantees these barriers require to preserve the illusion of sequential consistency.
Of course, barriers are still expensive on weaker processors, and people want to avoid them if possible. And some people noted that certain architectures--e.g., ARM and PPC--the hardware will guarantee the correct ordering of dependent loads and stores without a barrier. So the committee looked for a barrier whose realization on those architectures would be, like acquire is on x86, absolutely nothing. Thus we get https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n26..., the first proposal for memory_order_consume. The first architecture mentioned is actually ARM, to motivate why control dependencies aren't included in the definition of memory_order_consume. Alpha is only mentioned, in the same breath as x86, in a note pointing out that it doesn't benefit from the proposed memory ordering. And if you read all of the subsequent papers on fixing memory_order_consume to make it usable [2], the entire motivation is not Alpha--where there is no meaningful difference from memory_order_acquire--but PPC, ARM, even Itanium.
Let me reiterate again because it's important. The entire raison d'être of memory_order_consume is to support hardware where its realization is to not insert a hardware barrier. The people who keep trying to fix it, even now, are--by their own words--motivated by a desire to make it implemented as such on hardware like ARM. That a barrier is needed for Alpha is incidental to the proposal; if the Alpha never existed, the same people would still introduce memory_order_consume for the purpose of introducing the requisite compiler-but-not-hardware-barrier.
> So we're back to "consume exists so you can write C++ code in 2023 to run on an architecture Compaq cancelled in 2003". Right?
Not at all. And if the words of the people who proposed this feature, talking about its applicability to most hardware other than Alpha, aren't enough to convince you otherwise, then I am truly at a loss.
[1] This, incidentally, is why relaxed atomics is such a specification mess: it's trying to specify the semantics of an intentional data race in a model that's fundamentally incompatible with data races.
[2] e.g., https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n43...
You... really did not. I asked for code that is correct on x86 or ARM with a consume load and incorrect without.
> I'm instead going to try to tackle head-on what I think is preventing you from understanding
I think I'd understand it much better if you gave me the concrete example that I'm asking for and which you keep saying exists. Frankly at this point I think I'm done being patient and waiting for the explanation in the expectation I'm going to learn something, it's clear it's not going to arrive. Mostly: I don't believe you, I think it's you that are failing to understand the issue and not me, and I'll go on with my more comfortable hardware-level understanding of memory ordering and continue to ignore the pontifications of those in the language standard community. Sorry if that's frustrating.
int i = load(p, memory_order_consume);
if (i == 3) {
int y = x[i];
}
If you replace the memory_order_consume with a regular load, or a memory_order_relaxed, your code will be broken, even on x86 or ARM. The compiler can, and in this case likely will (yay constant propagation), rewrite the subsequent load in such a way that it is no longer data-dependent on the atomic load. It is not a compiler bug; if you try to report it as such, the compiler implementers will take one look at your problem report, and tell you that it is your code that is broken, use memory_order_consume. You will similarly get no sympathy from the C or C++ standards committees, for this is precisely why memory_order_consume was invented.> Mostly: I don't believe you, I think it's you that are failing to understand the issue and not me
As someone who works on a compiler, and who sits on language standards committees, when I am explaining to you how someone who works on a compiler and who sits on language standards committees will read the standard, it's not because I fail to understand how to do so. I do realize that the way the standards are read and interpreted does not come naturally to most, which is why I gave that long explanation. Perhaps you do not wish to engage with the specification on this level of understanding. That is fine--but a consequence is that you will not be able to properly understand the reason for certain features, like memory_order_consume.
At the very least, I hope you can agree to stop spreading misinformation like "the purpose of memory_order_consume is to support the Alpha processor."
int i = load(p, memory_order_consume);
if (i == 3) {
int y = load(x[i], memory_order_consume);
}
it seems like bad design to require ordering between plain loads and annotated loads. We want to identify the two locations that are important, so the compiler can reorder the others however it sees fit, without regard as to their placement w.r.t. those two.This is is catastrophic, if, instead of relying on an hardware barrier, we rely on a load-load dependency chain originating from i; rewriting x[i] into x[3] break the chain and now, while the compiler might still not reorder the access, the hardware very much can.
If its name is DEC Alpha? ;)
Of course "do what gcc x.y does" is not a good spec so the standard tried to specify exactly the behavior of consume is, but it failed to be implementable.
Right. Solely so it can support DEC Alpha! I'm not saying data dependence has never been an ordering constraint, I'm saying it was only an ordering constraint on an ancient platform that thus doesn't belong in the standard.
And by extension I'm saying that you people talking about standards instead of hardware are needlessly (frankly: deliberately and obfuscatorily) obscuring that clear truth.
You must use one of the rcu_dereference() family of primitives
to load an RCU-protected pointer, otherwise CONFIG_PROVE_RCU
will complain. Worse yet, your code can see random memory-corruption
bugs due to games that compilers and DEC Alpha can play.
Without one of the rcu_dereference() primitives, compilers
can reload the value, and won't your code have fun with two
different values for a single pointer! Without rcu_dereference(),
DEC Alpha can load a pointer, dereference that pointer, and
return data preceding initialization that preceded the store of
the pointer.
The whole document then goes in great details on the all the ways the compiler can screw you over.If the kernel were to drop Alpha support tomorrow, rcu_dereference and all the warnings would still be relevant.
Similarly, if only x86, SPARC and Alpha existed, memory order relaxed, acquire/release and seq-cst would be sufficient. consume only exists to cater to architectures with memory models stronger than Alpha but weaker than TSO.
[1]https://www.kernel.org/doc/Documentation/RCU/rcu_dereference...
Just because the simplest translation to assembly yields a data dependency on ARM et al doesn't mean the compiler is obligated to honor that.
The focus on standards is precisely because that's the only contract the compiler gives you. If you write code expecting to also get your target architecture's guarantees for free, you're writing software that is at best only incidentally correct until the next compiler update.
I look around this thread and I don't see a group of people trolling you - I genuinely think you're missing the point.
You keep saying that somehow this isn't about alpha, but it's clear this is about alpha, because alpha needs a barrier there and volatile can't do barriers. You just... won't admit it. And per my point WAY upthread, I contend you won't admit it because you view the "standard" as the important thing and not the hardware.
Even if volatile was absolutely all you needed, why would the committee build a memory model with a nice API and then force you to sprinkle in volatile? It makes absolutely no sense.
You can build yourself an access API if you have volatile. E.g.
#define volatile_lvalue(TYPE, LOC) (*(volatile TYPE *) &(LOC))
With typeof or _Generic, we can eliminate the TYPE argument.Then you can do:
if (volatile_lvalue(int, i) == 3) {
y = x[i]; // don't care if i replaced with 3 here
}
The user of volatile_lvalue doesn't have to declare anything volatile.In practice the volatile cast might prevent current GCCs from performing constant propagation, so this exact code sequence might work, but it is extremely fragile and can break for some variations:
if (int i2 = volatile_lvalue(int, i); i2 == 3) {
y = x[i2]; // i2 is replaced, no data dependency
}
or if (volatile_lvalue(int, i)) {
y = x[i-i]; // data dependency becomes a control dependency
}
You also have to be careful to what you do with y and with any value derived form it.As currently there is no way to efficiently implement consume you have to do with what you have, so for example the Linux kernel does use volatile casts and compilation barriers to implement LOAD_ONCE, but then has a large manual that documents what operations are currently considered safe and what are known to break.
That's hardly a specification that can be put in a standard and you can't really can reason about the semantics of it. Linus himself has considered multiple time if it is worth the hassle and whether the kernel should just switch to acquire everywhere now that arm and power have relevant cheap barriers.
volatile int i;
volatile int *x;
if (i == 3) {
y = x[i];
}
Even if this becomes x[3], volatile should work well enough to prevent the reordering: the access to x[] shouldn't precede the access to i. Now volatile might cause two accesses to i. That's my problem, which I could try to defeat with caching: int local_i = i;
if (local_i == 3)
y = x[3]; // Let's do this ourselves myself
Still, everything should be cool; the access to i and to x[3] should be in that order thanks to volatile. We have one access to i.If, in spite of the compiler obeying our volatile, the processor and memory hardware is rordering our accesses, then we need some fancy primitive, because special instructions have to be used.
The fancy primitive should exist only for the hardware problem, not for the compiler's ordering of instructions.
[1] I'm also ignoring the fact that volatile really doesn't have the right semantics in C++11/C11.
So let's not fix that, but invent new cruft.
C defines volatile for several purposes of its own, and that's it.
- In a function that uses setjmp, volatile must be used to mark variables that are modified after the context is saved with setjmp but before control returns with longjmp. Otherwise there is a problem, like the longjmp spuriously restoring the original values of those variables (due to those variables having been pulled into the machine state being saved and restored, like registers).
- When an asynchronous signal handler (e.g. for SIGINT) sets a global variable which is checked by the interrupted code, that variable has to be "volatile sig_atomic_t".
Any usefulness of volatile for actual concurrency is courtesy of the compiler, whether empirical or documented.
As an extension, MSVC did for a time add atomic and acquire/release semantics to volatile. This was deprecated because it broke too much stuff.
edit: after your edit, I'm not sure if you arguing for volatile-for-atomic or not.
Indeed I've worked with hardware where it was more obvious how to lower an atomic load or store than it was to lower a volatile load or store, because the technical definition of volatile really doesn't comport with the practical meaning of "don't optimize this" (crazy hardware does things like that).
most platforms with weaker-than-x86 memory models need additional barriers for acquires e.g. on ARM you need LDAR
> Probably the biggest reason that memory ordering semantics are such a mess
are they though? release/acquire semantics are actually intuitive once you get used to them and are very well suited for pointer-based lockfree data structures. consume is maybe more subtle but it's mostly an optimization on top of acquire, so not very difficult conceptually
In practice all non-TSO machines that need a load-load barrier for acquire but no barrier for data dependencies (i.e. loads through pointers). Notably this includes ARM and POWER. These architectures still needed relatively expensive fences on the release path.
On TSO machines (SPARC, x86 for example) acquire loads and release stores are free.
Alpha was the strange outlier that also needed a barrier for the dependent load path.
But acquire and release are such a sweet spot for programming that ARM (and apparently POWER) have since added cheap (although not free) acquire load/release stores.
Far from being a mess, the C++11 memory model (and acquire/release in particular) has been an unmitigated success that has allowed writing, discussing and proving the correctness of very practical lock free algorithms on real hardware while not resorting to the expensive SEQ-CST model. Also since it inception, all relevant architectures have formalized their memory model and made sure that they can implement acq/rel efficiently.
Acquire is too heavy, and Relaxed is too relaxed, even if it often works on non-Alpha CPUs in practice, it assumes the compiler doesn't get too clever with speculative prefetch and reuse optimisations.
Do we really still need to use Linux's compiler-specific `rcu_dereference` in performant code just because Consume isn't implemented?
> If your compiler predates C++20, however, the consume memory order is almost certainly treated as acquire (legal, just not faster). C++17 temporarily discouraged the use of the consume order because the semantics were impossible to use correctly, and thus all compilers were implementing it as acquire. > > The revisions in C++20 took some very smart people a lot of effort, and changed the definitions subtly so that you can use consume and get weaker memory ordering than acquire without immediately invoking our good friend UB.
> Implementations have found it infeasible to provide performance better than that of memory_order :: acquire.
I suspect that the comment is well-meant but is referring to some proposal that may have been made at some point in the last say five years, and which isn't in the ISO standard. Plenty of people have proposed that Consume could be fixed, but there are big gaps between proposing a fix, agreeing it among all interested parties and writing that fix into the standard, and actually shipping compilers with working Consume memory order.
Alas, it seems like consume was too complicated for compiler writers to take full advantage of. So in practice, acquire is used instead. And then in ARMv8 and POWER9, acquire was added to those instruction sets. Acquire isn't the default, but we do have accelerated assembly language support for the acquire model.
So moving forward, acquire seems to be the sweetspot model. I guess consume exists for a hypothetical future C++ compiler that tries to optimize multithreaded ARMv7 or something, a relatively small niche in practice as current C++ compilers don't support consume and future processors have moved on to the acquire model.
But in this case I believe the choice to have a default was itself the error. I have been persuaded that either you know which Ordering you need, and thus you should specify what you meant - or else you should not be using these APIs at all. If you aren't confident of the correct Ordering, you are not in fact confident your algorithm is correct, so don't do it.
Then again, I usually target x86 where these defaults are optimal, in other architectures relaxing RMWs further has benefits.
Because that's how typical programmers think, it is the default. Relaxed, consume, and acquire are invented for the people who are trying to save those precious nanoseconds by cutting out a potential memory barrier from the resulting code.
In the pre-C++11 days, I would just shove full barriers everywhere in my code defensively, cause I didn't really understand memory models. Seq_cst is basically that behavior (a full memory barrier between important operations) which is always correct but maybe a bit slow.
seq_cst / atomics / barriers are for writing lock-free code, which is fully compatible with interrupts and other system-call level details. Its about correctness, not about speed.
--------
I had a discussion elsewhere where people seem to think that these constructs are for speed. They... really aren't. Atomics are about lock-free code, and lock-free code might be slower than locked-code/mutex code.
Lock-free code is about forward progress in all conditions. Sometimes its faster, but its an independent axis as far as software design is concerned.
I guess acquire is faster than seq_cst, and consume/relaxed would be even faster than acquire. But correctness comes first, and proving correctness in consume-style code is seemingly too difficult in practice. So acquire is where the sweet spot seems to be... at least with today's tech and understanding of multithreaded issues.
Lock-free code by itself doesn't guarantee forward progress. I've seen way too much atomic code where the author sprinkles a seemingly infinite loop that only breaks when compare_exchange_strong succeeds. How would you reason that those loops will in fact terminate? When you use a mutex, there are no loops anywhere. Afraid of deadlocks making in mutex code? There are many good deadlock detection tools that you can use without directly reasoning about each section of atomic code. That's infinitely better in terms of productivity.
No. You literally cannot use a mutex in interrupt service routines.
If you fail to understand why, then you don't understand fundamental OS and/or device driver issues and need to study up on that a bit more. This isn't something you use userland tools to solve either.
You're arguing for a methodology in a use case where its literally impossible to use mutexes and/or locking. The only reasonable software engineering decision here is to write lock-free code with guarantees of forward progress in these situations.
Its not easy. A lot of people mess it up. But that's what you have to do. No one ever said kernel hacking was easy. Someone has to write this code, and whoever does will likely use a lock-free methodology. This is where atomics and barriers shine.
> Lock-free code by itself doesn't guarantee forward progress.
I don't think you understand what lock-free code means. It literally means code that has guarantees for forward progress.
Its very difficult to write lock-free code. Very, very difficult. But the methodology is well known and well documented. (https://preshing.com/20120612/an-introduction-to-lock-free-p...)
You use these guarantees of forward progress (ex: on a 128-core computer, if all are doing compare_exchange_strong at the same time, then 1x of them will succeed, and 127x of them will fail. This is guaranteed, _SOMEONE_ succeeds and makes forward progress with lock-free primitives like compare_exchange_strong).
That means that if all 128x threads/cores call "compare_exchange_strong" 1x each, then after 128x calls every thread will have succeeded (ie: worst case, one thread will have to call it 128x times).
No. This isn't sufficient to have lock-free code yet. You now need to prove that you don't deal with live-lock issues or other such complex issues that come up (and you'll only get here in the first place if you wrote your memory-barriers correctly and lined everything else up perfectly).
But its a start. And with enough effort, study, sweat and tears, you'll be able to write a lock-free something and have a good functioning interrupt service routine.
> Lock-free code by itself doesn't guarantee forward progress.
I think we have some definition differences here. What I have seen and have alluded to previously when you use tools like compare_exchange_strong is that the underlying machine does not guarantee forward progress. When you have 128 cores calling compare_exchange_strong at the same time, yes 1 of them will succeed and 127 of them will fail. But there is no guarantee that for a particular core, it will eventually succeed; it could always be among the 127 failing ones. I often see that on NUMA systems where many cores running on a single CPU are competing against one core running on a remote CPU. Due to the way hardware works, that remote CPU consistently takes longer to do each atomic operation, leading it to always fail.
So no, after 128x calls not every thread will succeed.
I don't think you understand the situation I setup. Lemme try again from first principles.
-----------
In general, as long as you have proven that there is a *FINITE* amount of work to do, then when you prove forward progress (ie: at least one thread makes progress), you prove forward progress for the whole system.
Even if the "slowest-thread" loses every single time, eventually the 127x other threads run out of work, and the final thread can finally call compare_exchange_strong by itself. When it is the only thread calling the atomic, it will succeed (no one else can contradict, because everyone else has run out of work and is out of the loop).
Its as simple as "finite work" + "forward progress" == "eventual completion". Even in an imbalanced system where thread #128 is 128x slower than thread#1 and "loses the race" every single time... eventually thread#1 runs out of work (it succeeded so many times, its done). And the other threads get to finally run.
-------
Writing code that guarantees forward progress is the hard part however.
That said, your comment is very illuminating and I thank you for it. I'm glad we've resolved our definitional and perspective differences.
I'd say lock-free programming doesn't necessarily help that problem. Performance improvements are one of the keys, as the faster your code executes, the "shorter the queues get" because you're processing everyone faster in the system.
Queuing Theory is one of the weirder maths in Comp. Sci. I have mixed opinions on whether or not its really worth studying, as it covers very obvious truths. (The faster your code executes, the shorter lines get. Etc. etc.). I've seen the arguments that Queuing Theory is a lot of "obvious math" that you'll keep solving over-and-over again in parallel systems / code optimization however, so might as well formalize the concepts.
-----------
Queuing Theory is that all systems can be modeled as queues (aka: lines), where people stand in line waiting to be served. And "servers", who serve these people waiting in line. When someone is served, they move on to another queue / line up and wait for the next server to serve them.
You then do a bunch of math to see how long these lines get (ie: representing how much RAM you need to save all of these jobs) in a variety of random-distributions (poisson arrival times, etc. etc.).
That's because no compiler implements it, it just becomes an acquire in practice. Nobody has figured out how to maintain all the necessary invariants across all optimization passes.
If you want a good overview of what it is supposed to do,
https://preshing.com/20140709/the-purpose-of-memory_order_co...
goes into more detail with lots of examples and motivation.
I don't really get this part. The docs for acquire say:
> memory_order_acquire A load operation with this memory order performs the acquire operation on the affected memory location: no reads or writes in the current thread can be reordered before this load. All writes in other threads that release the same atomic variable are visible in the current thread
The prefetch is already ordered before the load in the same thread.