Skip to content

Commit 43ca2e8

Browse files
author
Maxim Egorushkin
committed
Fix README typos, heading hierarchy, and formatting.
1 parent f692709 commit 43ca2e8

1 file changed

Lines changed: 58 additions & 48 deletions

File tree

README.md

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -48,35 +48,39 @@ Ultra-low-latency applications need just that and nothing more. The minimalism p
4848

4949
## Role Models
5050
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
6467
The containers provided are header-only class templates, no building/installing is necessary.
6568

66-
## Install from GitHub
69+
### Install from GitHub
6770
1. Clone the project:
68-
```
69-
git clone https://github.com/max0x7ba/atomic_queue.git
70-
```
71+
```bash
72+
git clone https://github.com/max0x7ba/atomic_queue.git
73+
```
7174
2. Add `atomic_queue/include` directory (use full path) to the include paths of your build system.
7275
3. `#include <atomic_queue/atomic_queue.h>` in your C++ source.
76+
7377
If you use CMake, these can be simplified as follows:
7478
```cmake
7579
add_subdirectory(atomic_queue)
7680
target_link_libraries(main PRIVATE atomic_queue::atomic_queue)
7781
```
7882

79-
## Using cmake FetchContent
83+
### Using cmake FetchContent
8084
You can also use CMake's FetchContent.
8185
```cmake
8286
include(FetchContent)
@@ -89,7 +93,7 @@ FetchContent_MakeAvailable(atomic_queue)
8993
target_link_libraries(main PRIVATE atomic_queue::atomic_queue)
9094
```
9195

92-
## Install using vcpkg
96+
### Install using vcpkg
9397
```
9498
vcpkg install atomic-queue
9599
```
@@ -99,16 +103,23 @@ find_package(atomic_queue CONFIG REQUIRED)
99103
target_link_libraries(main PRIVATE atomic_queue::atomic_queue)
100104
```
101105

102-
## Install using conan
106+
### Install using conan
103107
Follow the official tutorial on [how to consume conan packages](https://docs.conan.io/2/tutorial/consuming_packages.html).
104108
Details specific to this library are available in [ConanCenter](https://conan.io/center/recipes/atomic_queue).
105109

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

113+
```bash
114+
git clone https://github.com/max0x7ba/atomic_queue.git
115+
cd atomic_queue
116+
make -r -j4 BUILD=debug run_tests
111117
```
118+
119+
### Benchmark build and run instructions
120+
The benchmarks require several third-party libraries to be cloned as sibling directories, as well as Intel TBB.
121+
122+
```bash
112123
git clone https://github.com/cameron314/concurrentqueue.git
113124
git clone https://github.com/cameron314/readerwriterqueue.git
114125
git clone https://github.com/mpoeter/xenium.git
@@ -119,30 +130,30 @@ make -r -j4 run_benchmarks
119130

120131
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`.
121132

122-
# Library contents
123-
## Available queues
133+
## Library contents
134+
### Available queues
124135
* `AtomicQueue` - a fixed size ring-buffer for atomic elements.
125136
* `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`.
126137
* `AtomicQueue2` - a fixed size ring-buffer for non-atomic elements.
127138
* `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`.
128139

129140
These containers have corresponding `AtomicQueueB`, `OptimistAtomicQueueB`, `AtomicQueueB2`, `OptimistAtomicQueueB2` versions where the buffer size is specified as an argument to the constructor.
130141

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

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

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

137-
## Queue schematics
148+
### Queue schematics
138149

139150
```
140151
queue-end queue-front
141152
[newest-element, ..., oldest-element]
142153
push() pop()
143154
```
144155

145-
## Queue API
156+
### Queue API
146157
The queue class templates provide the following member functions:
147158
* `try_push` - Appends an element to the end of the queue. Returns `false` when the queue is full.
148159
* `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,
161172

162173
See [example.cc](src/example.cc) for a usage example.
163174

164-
# Implementation Notes
165-
## Memory order of non-atomic loads and stores
175+
## Implementation Notes
176+
### Memory order of non-atomic loads and stores
166177
`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:
167178
* 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`.
168179
* 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`.
169180
* 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.
170181

171-
## Ring-buffer capacity
182+
### Ring-buffer capacity
172183
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.
173184

174185
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
186197

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

189-
## Lock-free guarantees
200+
### Lock-free guarantees
190201
*Conceptually*, a `push` or `pop` operation does two atomic steps:
191202

192203
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.
193204
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.
194205

195206
These queues anticipate that a thread doing `push` or `pop` may complete step 1 and then be preempted before completing step 2.
196207

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:
198209

199210
* Each `push` is independent of any preceding `push`. An incomplete (preempted) `push` by one producer thread doesn't affect `push` of any other thread.
200211
* Each `pop` is independent of any preceding `pop`. An incomplete (preempted) `pop` by one consumer thread doesn't affect `pop` of any other thread.
201212
* 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.
202213
* 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.
203214

204-
## Preemption
215+
### Preemption
205216
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.
206217

207218
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_
216227

217228
[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.
218229

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

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

223-
# Benchmarks
234+
## Benchmarks
224235
[View throughput and latency benchmarks charts][1].
225236

226-
## Methodology
237+
### Methodology
227238
There are a few OS behaviours that complicate benchmarking:
228239
* CPU scheduler can place threads on different CPU cores each run. To avoid that the threads are pinned to specific CPU cores.
229240
* 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
234245

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

237-
### Huge pages
248+
#### Huge pages
238249
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
240251
sudo hugeadm --pool-pages-min 1GB:1
241252
sudo hugeadm --pool-pages-min 2MB:16
242253
```
243254
Alternatively, you may like to enable [transparent hugepages][15] in your system and use a hugepage-aware allocator, such as [tcmalloc][14].
244255

245-
### Real-time thread throttling
256+
#### Real-time thread throttling
246257
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
248259
echo -1 | sudo tee /proc/sys/kernel/sched_rt_runtime_us >/dev/null
249260
```
250261

251-
## Throughput and scalability benchmark
262+
### Throughput and scalability benchmark
252263
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.
253264

254-
## Ping-pong benchmark
265+
### Ping-pong benchmark
255266
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.
256267

257-
# Contributing
268+
## Contributing
258269
Contributions are more than welcome. `.editorconfig` and `.clang-format` can be used to automatically match code formatting.
259270

260-
# Reading material
271+
## Reading material
261272
Some books on the subject of multi-threaded programming I found quite instructive:
262273

263274
* _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
278289
[9]: https://stackoverflow.com/a/25168942/412080
279290
[10]: https://en.cppreference.com/w/cpp/atomic/atomic/is_lock_free
280291
[11]: https://en.cppreference.com/w/cpp/language/type
281-
[12]: https://en.cppreference.com/w/cpp/types/is_arithmetic
282292
[13]: https://en.cppreference.com/w/cpp/error/assert
283293
[14]: https://google.github.io/tcmalloc/temeraire.html
284294
[15]: https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html

0 commit comments

Comments
 (0)