Girls just wanna have fast MPMC queues with bounded waiting(nahla.dev)
nahla.dev
Girls just wanna have fast MPMC queues with bounded waiting
https://nahla.dev/blog/waitfree_queue/
48 comments
Thanks. I jumped at the headline. I'd be happy with wait-free MPSC. I haven't checked in for a while. Have there been any breakthroughs in low-complexity wait-free queues in the past 10 years?
The closest thing I know of, is that there was a concurrent queue algo called LCRQ
It originally required double-width CAS, but IIRC in recent years someone figured out how to remove this to make it more portable
Best reference I could find from cursory google:
https://ppopp23.sigplan.org/details/PPoPP-2023-papers/2/The-...
It originally required double-width CAS, but IIRC in recent years someone figured out how to remove this to make it more portable
Best reference I could find from cursory google:
https://ppopp23.sigplan.org/details/PPoPP-2023-papers/2/The-...
https://nikitakoval.org/publications/ppopp23-lprq.pdf seems to be the paper in question.
I suspect the search space of low-complexity, or at least what I'd consider "low-complexity", wait-free queues is pretty much exhausted at this point.
The MPSC/MPMC structure in Aeron is wait-free with respect to producers - one producer cannot block another
There are simple node based algorithms that achieves a similar guarantee:
https://web.archive.org/web/20240928080729/https://www.1024c...
There is also a MPMC algorithm on this site very similar to the article
https://web.archive.org/web/20220524214823/https://www.1024c...
There are simple node based algorithms that achieves a similar guarantee:
https://web.archive.org/web/20240928080729/https://www.1024c...
There is also a MPMC algorithm on this site very similar to the article
https://web.archive.org/web/20220524214823/https://www.1024c...
Nice to see Vyukov's MPMC queue mentioned. It's pretty neat. I have used a C implementation[1] of this in a small personal project.
[1]: https://github.com/dorjoy03/dsync/blob/master/src/mpmc_queue...
[1]: https://github.com/dorjoy03/dsync/blob/master/src/mpmc_queue...
Both Vyukov queues are fast and useful in practice, but neither is even obstruction-free, let alone lock-free or wait-free.
What's important is you know the trade-offs you are making.
You can't have a bounded queue that is always non-blocking because slow consumers can block producers.
You can't have a global FIFO order + multiple producers without slow producers blocking consumers.
You can't have a global FIFO order + have have non-atomic reserve and commit without a interrupted/de-scheduled producer thread being able to block the consumer
If you want atomic commit then you lose separate reserve which means either unbounded memory or atomic fixed-size data with sentinel values, ABA problems etc.
There are trade-offs everywhere, and it's best to pick the data structure that fits your needs just like any other problem.
You can't have a bounded queue that is always non-blocking because slow consumers can block producers.
You can't have a global FIFO order + multiple producers without slow producers blocking consumers.
You can't have a global FIFO order + have have non-atomic reserve and commit without a interrupted/de-scheduled producer thread being able to block the consumer
If you want atomic commit then you lose separate reserve which means either unbounded memory or atomic fixed-size data with sentinel values, ABA problems etc.
There are trade-offs everywhere, and it's best to pick the data structure that fits your needs just like any other problem.
> There are trade-offs everywhere, and it's best to pick the data structure that fits your needs just like any other problem.
That part I think is most crucial. Neither "Lock-free" nor "Wait-free" are vague terms for how awesome a thing is, they're specific properties which are expensive to provide, if you need such a property it was indispensable, if you don't need it then you can likely do better without it.
That part I think is most crucial. Neither "Lock-free" nor "Wait-free" are vague terms for how awesome a thing is, they're specific properties which are expensive to provide, if you need such a property it was indispensable, if you don't need it then you can likely do better without it.
Exactly. I mentioned that those queues aren't formally obstruction-free because the context of the conversation was new developments in wait-free queues, even though I have only needed the guarantee once in my career and end up using descendants of the Vyukov MPMC cycle queue in practically all other cases because they are better on the metrics that count, like speed.
What was the one time when you need something wait free? I'm assuming interacting with hardware?
Hard real-time industrial automation. Worst job ever, by the way.
This paper [0] from 2022 is pretty good. "Low complexity" it is not, though.
[0] https://arxiv.org/pdf/2201.02179
[0] https://arxiv.org/pdf/2201.02179
Not a ton, the inexorable march to wider scalar dispatch, deeper pipelines, and ever less uniform geometries (I pine for the halcyon days of two NUMA modes with a QPI bar) has made asymmetrical fencing juicy enough to be be worth the squeeze at the margins, you weren't seeing a ton of `asm volatile ""` on one side and `membarrier()` on the other a decade ago and you'll see that now.
But I think Nathan Bronson's work out of IIRC Standford about 10 or 15 years ago is still more or less the canvas you paint on.
But I think Nathan Bronson's work out of IIRC Standford about 10 or 15 years ago is still more or less the canvas you paint on.
Surprised nobody here nor on the reddit thread caught this one: https://github.com/nahla-nee/wfqueue/blob/main/src/lib.rs#L3...
The fix is straightforward:
Actually, later on, the code makes a similar mistake, but only for one impl.
unsafe impl<T, const N: usize> Sync for WFQueue<T, N> {}
unsafe impl<T, const N: usize> Send for WFQueue<T, N> {}
These impls are unsound, because neither constrains `T` to be `Sync`/`Send`. As-written this would let you declare a `WFQueue<Rc<T>, N>` and pass non-atomic-refcount pointers between threads.The fix is straightforward:
unsafe impl<T: Sync, const N: usize> Sync for WFQueue<T, N> {}
unsafe impl<T: Send, const N: usize> Send for WFQueue<T, N> {}
I.e., WFQueue is only Sync if T is Sync, and likewise for Send.Actually, later on, the code makes a similar mistake, but only for one impl.
unsafe impl<'a, T, const N: usize> Send for DrivableWFEnqueue<'a, T, N> where T: Send {}
unsafe impl<'a, T, const N: usize> Send for DrivableWFDequeue<'a, T, N> {}WFQueue isn't handing out &T, so Send is sufficient:
unsafe impl<T: Send, const N: usize> Sync for WFQueue<T, N> {}
unsafe impl<T: Send, const N: usize> Send for WFQueue<T, N> {}Here's my widely used implementation of this approach in C++: https://github.com/rigtorp/MPMCQueue
This was the very best bounded MPMC queue when I last looked into these things years ago, and as far as descendants of the Vyukov MPMC cycle queue go, I don't think it's possible to do much better.
I think your citation date is off, by the way. As far as I can tell, it was first published in January 2011.
I think your citation date is off, by the way. As far as I can tell, it was first published in January 2011.
On the topic of lock free data structures I found this one on a SPSC very interesting too https://david.alvarezrosa.com/posts/optimizing-a-lock-free-r... taking it from 12M to 305M ops/s
That looks like a rewrite of my earlier work: https://rigtorp.se/ringbuffer/
Perhaps I missed it but there didn't appear to be discussion of false sharing between the N individual data slots. It might be beneficial to pad each slot to a cache line width (or at least less slots per line), and/or using some kind of bijective hashing on the slot lookup so that sequential tickets don't access adjacent slots.
You would use one of those approaches:
If you align and pad each slot there won't be any false sharing and the stream prefetcher can kick in if there's only one producer or consumer.
If you use bijective hashing you reduce false sharing without aligning and padding. This can save memory at the expense of the stream prefetcher never kicking in.
If you align and pad each slot there won't be any false sharing and the stream prefetcher can kick in if there's only one producer or consumer.
If you use bijective hashing you reduce false sharing without aligning and padding. This can save memory at the expense of the stream prefetcher never kicking in.
>fast MPMC queues with bounded waiting
Sounds like fun.
Sounds like fun.
It's very rare (if ever) I'd want M on both ends... and no work stealing. Usually it's one M and an S.
throw8384949(4)
Classy disclaimer! matthieum's (long) reddit comment is also an informative read: https://www.reddit.com/r/rust/comments/1up0uhg/girls_just_wa...