You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+58-48Lines changed: 58 additions & 48 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -48,35 +48,39 @@ Ultra-low-latency applications need just that and nothing more. The minimalism p
48
48
49
49
## Role Models
50
50
Several other well established and popular thread-safe containers are used for reference in the [benchmarks][1]:
51
-
*`std::mutex` - a fixed size ring-buffer with `std::mutex`.
52
-
*`pthread_spinlock` - a fixed size ring-buffer with `pthread_spinlock_t`.
53
-
*`boost::lockfree::spsc_queue` - a wait-free single-producer-single-consumer queue from Boost library.
54
-
*`boost::lockfree::queue` - a lock-free multiple-producer-multiple-consumer queue from Boost library.
55
-
*`moodycamel::ConcurrentQueue` - a lock-free multiple-producer-multiple-consumer queue used in non-blocking mode. This queue is designed to maximize throughput at the expense of latency and eschewing the global time order of elements pushed into one queue by different threads. It is not equivalent to other queues benchmarked here in this respect.
56
-
*`moodycamel::ReaderWriterQueue` - a lock-free single-producer-single-consumer queue used in non-blocking mode.
57
-
*`xenium::michael_scott_queue` - a lock-free multi-producer-multi-consumer queue proposed by [Michael and Scott](http://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf) (this queue is similar to `boost::lockfree::queue` which is also based on the same proposal).
58
-
*`xenium::ramalhete_queue` - a lock-free multi-producer-multi-consumer queue proposed by [Ramalhete and Correia](http://concurrencyfreaks.blogspot.com/2016/11/faaarrayqueue-mpmc-lock-free-queue-part.html).
59
-
*`xenium::vyukov_bounded_queue` - a bounded multi-producer-multi-consumer queue based on the version proposed by [Vyukov](https://groups.google.com/forum/#!topic/lock-free/-bqYlfbQmH0).
60
-
*`tbb::spin_mutex` - a locked fixed size ring-buffer with `tbb::spin_mutex` from Intel Threading Building Blocks.
61
-
*`tbb::concurrent_bounded_queue` - eponymous queue used in non-blocking mode from Intel Threading Building Blocks.
62
-
63
-
# Using the library
51
+
52
+
| Queue | Type | Description |
53
+
|-------|------|-------------|
54
+
|`std::mutex`| MPMC | A fixed size ring-buffer with `std::mutex`. |
55
+
|`pthread_spinlock`| MPMC | A fixed size ring-buffer with `pthread_spinlock_t`. |
56
+
|`boost::lockfree::spsc_queue`| SPSC | A wait-free queue from Boost library. |
57
+
|`boost::lockfree::queue`| MPMC | A lock-free queue from Boost library. |
58
+
|`moodycamel::ConcurrentQueue`| MPMC | A lock-free queue used in non-blocking mode. Designed to maximize throughput at the expense of latency, eschewing global time order. Not equivalent to other queues benchmarked here in this respect. |
59
+
|`moodycamel::ReaderWriterQueue`| SPSC | A lock-free queue used in non-blocking mode. |
60
+
|`xenium::michael_scott_queue`| MPMC | A lock-free queue proposed by [Michael and Scott](http://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf) (similar to `boost::lockfree::queue` which is also based on the same proposal). |
61
+
|`xenium::ramalhete_queue`| MPMC | A lock-free queue proposed by [Ramalhete and Correia](http://concurrencyfreaks.blogspot.com/2016/11/faaarrayqueue-mpmc-lock-free-queue-part.html). |
62
+
|`xenium::vyukov_bounded_queue`| MPMC | A bounded queue based on the version proposed by [Vyukov](https://groups.google.com/forum/#!topic/lock-free/-bqYlfbQmH0). |
63
+
|`tbb::spin_mutex`| MPMC | A locked fixed size ring-buffer with `tbb::spin_mutex` from Intel Threading Building Blocks. |
64
+
|`tbb::concurrent_bounded_queue`| MPMC | Eponymous queue used in non-blocking mode from Intel Threading Building Blocks. |
65
+
66
+
## Using the library
64
67
The containers provided are header-only class templates, no building/installing is necessary.
Follow the official tutorial on [how to consume conan packages](https://docs.conan.io/2/tutorial/consuming_packages.html).
104
108
Details specific to this library are available in [ConanCenter](https://conan.io/center/recipes/atomic_queue).
105
109
106
-
## Benchmark build and run instructions
107
-
The containers provided are header-only class templates that require only `#include <atomic_queue/atomic_queue.h>`, no building/installing is necessary.
108
-
109
-
Building is necessary to run the tests and benchmarks.
110
+
### Test build and run instructions
111
+
Building is necessary to run the tests and benchmarks. The tests require the Boost.Test library (e.g. `libboost-test-dev` on Debian/Ubuntu).
The benchmark also requires Intel TBB library to be available. It assumes that it is installed in `/usr/local/include` and `/usr/local/lib`. If it is installed elsewhere you may like to modify `cppflags.tbb` and `ldlibs.tbb` in `Makefile`.
121
132
122
-
# Library contents
123
-
## Available queues
133
+
##Library contents
134
+
###Available queues
124
135
*`AtomicQueue` - a fixed size ring-buffer for atomic elements.
125
136
*`OptimistAtomicQueue` - a faster fixed size ring-buffer for atomic elements which busy-waits when empty or full. It is `AtomicQueue` used with `push`/`pop` instead of `try_push`/`try_pop`.
126
137
*`AtomicQueue2` - a fixed size ring-buffer for non-atomic elements.
127
138
*`OptimistAtomicQueue2` - a faster fixed size ring-buffer for non-atomic elements which busy-waits when empty or full. It is `AtomicQueue2` used with `push`/`pop` instead of `try_push`/`try_pop`.
128
139
129
140
These containers have corresponding `AtomicQueueB`, `OptimistAtomicQueueB`, `AtomicQueueB2`, `OptimistAtomicQueueB2` versions where the buffer size is specified as an argument to the constructor.
130
141
131
-
Totally ordered mode is supported. In this mode consumers receive messages in the same FIFO order the messages were posted. This mode is supported for `push` and `pop` functions, but for not the `try_` versions. On Intel x86 the totally ordered mode has 0 cost, as of 2019.
142
+
Totally ordered mode is supported. In this mode consumers receive messages in the same FIFO order the messages were posted. This mode is supported for `push` and `pop` functions, but not for the `try_` versions. On Intel x86 the totally ordered mode has 0 cost, as of 2019.
132
143
133
144
Single-producer-single-consumer mode is supported. In this mode, no expensive atomic read-modify-write CPU instructions are necessary, only the cheapest atomic loads and stores. That improves queue throughput significantly.
134
145
135
-
Move-only queue element types are fully supported. For example, a queue of `std::unique_ptr<T>` elements would be `AtomicQueue2B<std::unique_ptr<T>>` or `AtomicQueue2<std::unique_ptr<T>, CAPACITY>`.
146
+
Move-only queue element types are fully supported. For example, a queue of `std::unique_ptr<T>` elements would be `AtomicQueueB2<std::unique_ptr<T>>` or `AtomicQueue2<std::unique_ptr<T>, CAPACITY>`.
136
147
137
-
## Queue schematics
148
+
###Queue schematics
138
149
139
150
```
140
151
queue-end queue-front
141
152
[newest-element, ..., oldest-element]
142
153
push() pop()
143
154
```
144
155
145
-
## Queue API
156
+
###Queue API
146
157
The queue class templates provide the following member functions:
147
158
*`try_push` - Appends an element to the end of the queue. Returns `false` when the queue is full.
148
159
*`try_pop` - Removes an element from the front of the queue. Returns `false` when the queue is empty.
@@ -161,14 +172,14 @@ Note that _optimism_ is a choice of a queue modification operation control flow,
161
172
162
173
See [example.cc](src/example.cc) for a usage example.
163
174
164
-
# Implementation Notes
165
-
## Memory order of non-atomic loads and stores
175
+
##Implementation Notes
176
+
###Memory order of non-atomic loads and stores
166
177
`push` and `try_push` operations _synchronize-with_ (as defined in [`std::memory_order`][17]) with any subsequent `pop` or `try_pop` operation of the same queue object. Meaning that:
167
178
* No non-atomic load/store gets reordered past `push`/`try_push`, which is a `memory_order::release` operation. Same memory order as that of `std::mutex::unlock`.
168
179
* No non-atomic load/store gets reordered prior to `pop`/`try_pop`, which is a `memory_order::acquire` operation. Same memory order as that of `std::mutex::lock`.
169
180
* The effects of a producer thread's non-atomic stores followed by `push`/`try_push` of an element into a queue become visible in the consumer's thread which `pop`/`try_pop` that particular element.
170
181
171
-
## Ring-buffer capacity
182
+
###Ring-buffer capacity
172
183
The available queues here use a ring-buffer array for storing elements. The capacity of the queue is fixed at compile time or construction time.
173
184
174
185
In a production multiple-producer-multiple-consumer scenario the ring-buffer capacity should be set to the maximum expected queue size. When the ring-buffer gets full it means that the consumers cannot consume the elements fast enough. A fix for that is any of:
@@ -186,22 +197,22 @@ The containers use `unsigned` type for size and internal indexes. On x86-64 plat
186
197
187
198
While the atomic queues can be used with any moveable element types (including `std::unique_ptr`), for best throughput and latency the queue elements should be cheap to copy and lock-free (e.g. `unsigned` or `T*`), so that `push` and `pop` operations complete fastest.
188
199
189
-
## Lock-free guarantees
200
+
###Lock-free guarantees
190
201
*Conceptually*, a `push` or `pop` operation does two atomic steps:
191
202
192
203
1. Atomically and exclusively claims the queue slot index to store/load an element to/from. That's producers incrementing `head` index, consumers incrementing `tail` index. Each slot is accessed by one producer and one consumer threads only.
193
204
2. Atomically store/load the element into/from the slot. Producer storing into a slot changes its state to be non-`NIL`, consumer loading from a slot changes its state to be `NIL`. The slot is a spinlock for its one producer and one consumer threads.
194
205
195
206
These queues anticipate that a thread doing `push` or `pop` may complete step 1 and then be preempted before completing step 2.
196
207
197
-
An algorithm is *lock-free* if there is guaranteed system-wide progress. These queue guarantee system-wide progress by the following properties:
208
+
An algorithm is *lock-free* if there is guaranteed system-wide progress. These queues guarantee system-wide progress by the following properties:
198
209
199
210
* Each `push` is independent of any preceding `push`. An incomplete (preempted) `push` by one producer thread doesn't affect `push` of any other thread.
200
211
* Each `pop` is independent of any preceding `pop`. An incomplete (preempted) `pop` by one consumer thread doesn't affect `pop` of any other thread.
201
212
* An incomplete (preempted) `push` from one producer thread affects only one consumer thread `pop`ing an element from this particular queue slot. All other threads `pop`s are unaffected.
202
213
* An incomplete (preempted) `pop` from one consumer thread affects only one producer thread `push`ing an element into this particular queue slot while expecting it to have been consumed long time ago, in the rather unlikely scenario that producers have wrapped around the entire ring-buffer while this consumer hasn't completed its `pop`. All other threads `push`s and `pop`s are unaffected.
203
214
204
-
## Preemption
215
+
###Preemption
205
216
Linux task scheduler thread preemption is something no user-space process should be able to affect or escape, otherwise any/every malicious application would exploit that.
206
217
207
218
Still, there are a few things one can do to minimize preemption of one's mission critical application threads:
@@ -216,14 +227,14 @@ People often propose limiting busy-waiting with a subsequent call to `std::this_
216
227
217
228
[In Linux, there is mutex type `PTHREAD_MUTEX_ADAPTIVE_NP`][9] which busy-waits a locked mutex for a number of iterations and then makes a blocking syscall into the kernel to deschedule the waiting thread. In the benchmarks it was the worst performer and I couldn't find a way to make it perform better, and that's the reason it is not included in the benchmarks.
218
229
219
-
C++20 introduced blocking `std::atomic::wait` which uses Linux futex for atomic compare-and-block operation, similar to `PTHREAD_MUTEX_ADAPTIVE_NP`, with hard-coded spin-count limits. And `thread_yield` calls, which Linus Torvalds above explains is only applicable for real-time threads with a single run-queue for them. A queue implemenation with `std::atomic::wait` is due to be benchmarked, with its performance expected to be similar to that of `PTHREAD_MUTEX_ADAPTIVE_NP`, but I'd love to be pleasantly surpised.
230
+
C++20 introduced blocking `std::atomic::wait` which uses Linux futex for atomic compare-and-block operation, similar to `PTHREAD_MUTEX_ADAPTIVE_NP`, with hard-coded spin-count limits. And `thread_yield` calls, which Linus Torvalds above explains is only applicable for real-time threads with a single run-queue for them. A queue implementation with `std::atomic::wait` is due to be benchmarked, with its performance expected to be similar to that of `PTHREAD_MUTEX_ADAPTIVE_NP`, but I'd love to be pleasantly surprised.
220
231
221
232
On Intel CPUs one could use [the 4 debug control registers][6] to monitor the spinlock memory region for write access and wait on it using `select` (and its friends) or `sigwait` (see [`perf_event_open`][7] and [`uapi/linux/hw_breakpoint.h`][8] for more details). A spinlock waiter could suspend itself with `select` or `sigwait` until the spinlock state has been updated. But there are only 4 of these registers, so that such a solution wouldn't scale.
222
233
223
-
# Benchmarks
234
+
##Benchmarks
224
235
[View throughput and latency benchmarks charts][1].
225
236
226
-
## Methodology
237
+
###Methodology
227
238
There are a few OS behaviours that complicate benchmarking:
228
239
* CPU scheduler can place threads on different CPU cores each run. To avoid that the threads are pinned to specific CPU cores.
229
240
* CPU scheduler can preempt threads. To avoid that real-time `SCHED_FIFO` priority 50 is used to disable scheduler time quantum expiry and make the threads non-preemptable by lower priority processes/threads.
@@ -234,30 +245,30 @@ Benchmark performance of single-producer-single-consumer queues `boost::lockfree
234
245
235
246
I only have access to a few x86-64 machines. If you have access to different hardware feel free to submit the output file of `scripts/run-benchmarks.sh` and I will include your results into the benchmarks page.
236
247
237
-
### Huge pages
248
+
####Huge pages
238
249
When huge pages are available the benchmarks use 1x1GB or 16x2MB huge pages for the queues to minimise TLB misses. To enable huge pages do one of:
239
-
```
250
+
```bash
240
251
sudo hugeadm --pool-pages-min 1GB:1
241
252
sudo hugeadm --pool-pages-min 2MB:16
242
253
```
243
254
Alternatively, you may like to enable [transparent hugepages][15] in your system and use a hugepage-aware allocator, such as [tcmalloc][14].
244
255
245
-
### Real-time thread throttling
256
+
####Real-time thread throttling
246
257
By default, Linux scheduler throttles real-time threads from consuming 100% of CPU and that is detrimental to benchmarking. Full details can be found in [Real-Time group scheduling][2]. To disable real-time thread throttling do:
247
-
```
258
+
```bash
248
259
echo -1 | sudo tee /proc/sys/kernel/sched_rt_runtime_us >/dev/null
249
260
```
250
261
251
-
## Throughput and scalability benchmark
262
+
###Throughput and scalability benchmark
252
263
N producer threads push a 4-byte integer into one same queue, N consumer threads pop the integers from the queue. All producers posts 1,000,000 messages in total. Total time to send and receive all the messages is measured. The benchmark is run for from 1 producer and 1 consumer up to `(total-number-of-cpus / 2)` producers/consumers to measure the scalability of different queues.
253
264
254
-
## Ping-pong benchmark
265
+
###Ping-pong benchmark
255
266
One thread posts an integer to another thread through one queue and waits for a reply from another queue (2 queues in total). The benchmarks measures the total time of 100,000 ping-pongs, best of 10 runs. Contention is minimal here (1-producer-1-consumer, 1 element in the queue) to be able to achieve and measure the lowest latency. Reports the average round-trip time.
256
267
257
-
# Contributing
268
+
##Contributing
258
269
Contributions are more than welcome. `.editorconfig` and `.clang-format` can be used to automatically match code formatting.
259
270
260
-
# Reading material
271
+
##Reading material
261
272
Some books on the subject of multi-threaded programming I found quite instructive:
262
273
263
274
*_Programming with POSIX Threads_ by David R. Butenhof.
@@ -278,7 +289,6 @@ Copyright (c) 2019 Maxim Egorushkin. MIT License. See the full licence in file L
0 commit comments