Live data from Hacker News

Optimizing a lock-free ring buffer

david.alvarezrosa.com

51–60 of 100 posts

Re: Optimizing a lock-free ring buffer

#51

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.

You ultimately need a mask to access the correct slot in the ring. But it's true that you can leave unmasked values in your reader/writer indices.

Re: Optimizing a lock-free ring buffer

#52
post #43

Earlier 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.

> 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

#53

Earlier 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.

> 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.

Re: Optimizing a lock-free ring buffer

#54
post #10

Earlier 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…

yeah that’s why i was surprised by grandparent saying the atomics were c++ specific

Re: Optimizing a lock-free ring buffer

#55
post #52

Earlier 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.)

That's a good point. They are very similar. I guess the sentinel design in theory doesn't need to synchronize at all as long as there is a decent buffer between them. But the cached design synchronizes less commonly the more space there is which sounds like it would be very similar in practice. The sentinel design might also have a few thrashing issues when the reader and writer are on the same page which would probably be a bit less of an issue with the cached index design.

Re: Optimizing a lock-free ring buffer

#56
post #53

Earlier 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.

Yep, you are right, more precise, and precision is very important in this topic.

I stand corrected.

Re: Optimizing a lock-free ring buffer

#57

Earlier 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 :)

See https://fgiesen.wordpress.com/2012/07/21/the-magic-ring-buff... which takes it even further :)

Re: Optimizing a lock-free ring buffer

#58
post #18

Earlier 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.

[flagged]

Re: Optimizing a lock-free ring buffer

#59

Earlier quoted context omitted.

Interesting, I've never heard about anybody using this. Maybe a bit unreadable? But yeah, should work :)

See https://fgiesen.wordpress.com/2012/07/21/the-magic-ring-buff... which takes it even further :)

Nice one!

Re: Optimizing a lock-free ring buffer

#60

Something 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?

kernel tickrate is a pretty big one, most people don't bother and use what their OS ships with.

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.

Post reply on HN