Earlier quoted context omitted.
If you enforce that the buffer size is a power of 2 you just use a mask to do the if (next_head == buffer.size()) next_head = 0; part
If it's a power of two, you don't need the branch at all. Let the unsigned index wrap.
Optimizing a lock-free ring buffer
51–60 of 100 posts
Re: Optimizing a lock-free ring buffer
#52Earlier quoted context omitted.
Yeah, or you could put a generation number in each slot adjacent to T and a read will only be valid if the slot's generation number == the last one observed + 1, for example. But ultimately the reader and writer still need to coordinate here, so we're just shifting the coordination cache line from the writer's index to the slot.
I think the key difference is that they only need to coordinate when the reader and writer are close together. If that slows one end down they naturally spread apart. So you don't lose throughput, only a little latency in the contested case.
This was already the case with the cached index design at the end of the article, though. (Which doesn't require extra space or extra atomic stores.)
Re: Optimizing a lock-free ring buffer
#53Earlier quoted context omitted.
Push: buffer_[head] = value; head_.store(next_head, std::memory_order_release); return true; There's no relationship between the two written variables. Stores to the two are independent and can be reordered. The aq/rel applies to the index, not to the unrelated non-atomic buffer located near the index.
write with release semantic cannot be reordered with any other writes, dependent or not. Relaxed atomic writes can be reordered in any way.
To quibble a little bit: later program-order writes CAN be reordered before release writes. But earlier program-order writes may not be reordered after release writes.
> Relaxed atomic writes can be reordered in any way.
To quibble a little bit: they can't be reordered with other operations on the same variable.
Re: Optimizing a lock-free ring buffer
#54Earlier quoted context omitted.
Really? Pretty much all atomics i’ve used have load, store of various integer sizes. I wrote a ring buffer in Go that’s very similar to the final design here using similar atomics. https://pkg.go.dev/sync/atomic#Int64
They generally map directly to concepts in the CPU architecture. On many architectures, load/store instructions are already guaranteed to be atomic as long as the address is properly aligned, so atomic load/store is just a load/store. Non-relaxed ordering may emit a variant load/store instruction or a separate barrier instruction. Compare-exchange will usually emit a compare and swap, or load-linked/store-conditional…
Re: Optimizing a lock-free ring buffer
#55Earlier quoted context omitted.
I think the key difference is that they only need to coordinate when the reader and writer are close together. If that slows one end down they naturally spread apart. So you don't lose throughput, only a little latency in the contested case.
> I think the key difference is that they only need to coordinate when the reader and writer are close together. This was already the case with the cached index design at the end of the article, though. (Which doesn't require extra space or extra atomic stores.)
Re: Optimizing a lock-free ring buffer
#56Earlier quoted context omitted.
write with release semantic cannot be reordered with any other writes, dependent or not. Relaxed atomic writes can be reordered in any way.
> write with release semantic cannot be reordered with any other writes, dependent or not. To quibble a little bit: later program-order writes CAN be reordered before release writes. But earlier program-order writes may not be reordered after release writes. > Relaxed atomic writes can be reordered in any way. To quibble a little bit: they can't be reordered with other operations on the same variable.
I stand corrected.
Re: Optimizing a lock-free ring buffer
#57Earlier quoted context omitted.
If it's a power of two, you don't need the branch at all. Let the unsigned index wrap.
Interesting, I've never heard about anybody using this. Maybe a bit unreadable? But yeah, should work :)
Re: Optimizing a lock-free ring buffer
#58Earlier quoted context omitted.
> It's obviously, trivially broken. Stores the index before storing the value, so the other thread reads nonsense whenever the race goes against it. Are we reading the same code? The stores are clearly after value accesses. > Also doesn't have fences on the store ?? It uses acquire/release semantics seemingly correctly. Explicit fences are not required.
Push: buffer_[head] = value; head_.store(next_head, std::memory_order_release); return true; There's no relationship between the two written variables. Stores to the two are independent and can be reordered. The aq/rel applies to the index, not to the unrelated non-atomic buffer located near the index.
Re: Optimizing a lock-free ring buffer
#59Re: Optimizing a lock-free ring buffer
#60Something to add to this; if you're focussing on these low-level optimizations, make sure the device this code runs on is actually tuned. A lot of people focus on the code and then assume the device in question is only there to run it. There's so much you can tweak. I don't always measure it, but last time I saw at least a 20% improvement in Network throughput just by tweaking a few things on the machine.
Agreed. For benchmarking I used this https://github.com/david-alvarez-rosa/CppPlayground/blob/mai... > which relies on GoogleBenchmark and pins producer/consumer threads to dedicated CPU cores What else could be improved? Would like to learn :) Maybe using huge pages?
Disabling c-states, pinning network interfaces to dedicated cores (and isolating your application from those cores) and `SCHED_FIFO` (chrt -f 99 ) helps a lot.
Transparent hugepages increase latency without you being aware of when it happens, I usually disable that.
Idk, there's a bunch but they all depend on your use-case. For example I always disable hyperthreading because I care more about latency than processing power- and I don't want to steal cache from my workload randomly.. but some people have more I/O bound workloads and hyperthreading is just and strict improvement in those situations.