Skip to content

Commit 7086328

Browse files
committed
Add comparative queue benchmark with moodycamel
Introduces a new benchmark (queue_comparison_benchmark.cpp) to compare the performance of LockFreeSpscQueue with moodycamel::ReaderWriterQueue using Google Benchmark. Updates CMake to add a SPSC_QUEUE_BUILD_BENCHMARK_COMPARE option, fetches moodycamel's queue if enabled, and builds the new benchmark executable. Existing benchmark logic is refactored to support conditional builds for standard and comparative benchmarks.
1 parent edd11cd commit 7086328

3 files changed

Lines changed: 323 additions & 12 deletions

File tree

CMakeLists.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ target_include_directories(spsc_queue INTERFACE
2727
option(SPSC_QUEUE_BUILD_TESTS "Build the project's tests" ON)
2828
option(SPSC_QUEUE_BUILD_EXAMPLES "Build the project's examples" ON)
2929
option(SPSC_QUEUE_BUILD_BENCHMARKS "Build the project's benchmarks" OFF)
30+
option(SPSC_QUEUE_BUILD_BENCHMARK_COMPARE "Build comparative benchmarks against other libraries" OFF)
3031

3132
# For MacOS
32-
if(APPLE AND (SPSC_QUEUE_BUILD_TESTS OR SPSC_QUEUE_BUILD_EXAMPLES OR SPSC_QUEUE_BUILD_BENCHMARKS))
33+
if(APPLE AND (SPSC_QUEUE_BUILD_TESTS
34+
OR SPSC_QUEUE_BUILD_EXAMPLES
35+
OR SPSC_QUEUE_BUILD_BENCHMARKS
36+
OR SPSC_QUEUE_BUILD_BENCHMARK_COMPARE))
3337
# Enable experimental Clang stdlib features (for std::jthreads) when
3438
# building tests, examples or benchmark
3539
# As of Apple Clang version 17.0.0, this is still required
@@ -46,7 +50,7 @@ if(SPSC_QUEUE_BUILD_EXAMPLES)
4650
add_subdirectory(examples)
4751
endif()
4852

49-
if(SPSC_QUEUE_BUILD_BENCHMARKS)
53+
if(SPSC_QUEUE_BUILD_BENCHMARKS OR SPSC_QUEUE_BUILD_BENCHMARK_COMPARE)
5054
add_subdirectory(benchmarks)
5155
endif()
5256

benchmarks/CMakeLists.txt

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file handles the building of performance benchmarks.
22

3-
# 1. Fetch Google Benchmark
3+
# 1. Fetch Google Benchmark (needed for all benchmarks)
44
# Use FetchContent to download and prepare Google Benchmark. This is the modern
55
# CMake approach and does not require the user to have it installed system-wide.
66
include(FetchContent)
@@ -20,13 +20,35 @@ FetchContent_MakeAvailable(GoogleBenchmark)
2020
# 2. Find the threading library
2121
find_package(Threads REQUIRED)
2222

23-
# 3. Define the benchmark executable
24-
add_executable(queue_benchmark queue_benchmark.cpp)
23+
# 3. Build the standard, internal benchmark if enabled
24+
if(SPSC_QUEUE_BUILD_BENCHMARKS)
25+
add_executable(queue_benchmark queue_benchmark.cpp)
26+
target_link_libraries(queue_benchmark PRIVATE
27+
spsc_queue
28+
benchmark::benchmark
29+
Threads::Threads
30+
)
31+
endif()
32+
33+
# 4. Build the new comparative benchmark if enabled
34+
if(SPSC_QUEUE_BUILD_BENCHMARK_COMPARE)
35+
# Fetch moodycamel::ConcurrentQueue
36+
FetchContent_Declare(
37+
MoodycamelConcurrentQueue
38+
GIT_REPOSITORY https://github.com/cameron314/readerwriterqueue.git
39+
GIT_TAG v1.0.7 # Use a specific tag for stability
40+
)
41+
FetchContent_MakeAvailable(MoodycamelConcurrentQueue)
42+
43+
# Define the comparative benchmark executable
44+
add_executable(queue_comparison_benchmark queue_comparison_benchmark.cpp)
45+
46+
# Link it against our queue, the moodycamel queue, Google Benchmark, and threads
47+
target_link_libraries(queue_comparison_benchmark PRIVATE
48+
spsc_queue
49+
readerwriterqueue
50+
benchmark::benchmark
51+
Threads::Threads
52+
)
53+
endif()
2554

26-
# 4. Link the executable against our queue, Google Benchmark, and threads.
27-
# The `benchmark::benchmark` target is created by FetchContent_MakeAvailable.
28-
target_link_libraries(queue_benchmark PRIVATE
29-
spsc_queue
30-
benchmark::benchmark
31-
Threads::Threads
32-
)
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
#include "LockFreeSpscQueue.h"
2+
#include "readerwriterqueue.h" // Moodycamel's queue header
3+
#include <benchmark/benchmark.h>
4+
#include <thread>
5+
#include <vector>
6+
#include <atomic>
7+
#include <numeric>
8+
#include <random>
9+
10+
using DataType = int64_t;
11+
constexpr size_t RandomDataSize = 4001; // The prime to make patterns less obvious
12+
constexpr size_t ItemsPerIteration = 100'025;
13+
constexpr size_t DefaultQueueCapacity = 65536; // 2^16
14+
15+
// A capacity guaranteed to be larger than ItemsPerIteration.
16+
constexpr size_t LargeQueueCapacity = 262144; // 2^18
17+
18+
// Shared Test Data Generation
19+
const std::vector<DataType>& get_random_data() {
20+
static const auto data = []{
21+
std::vector<DataType> d(RandomDataSize);
22+
std::mt19937_64 rng(33317); // Fixed seed for test stability
23+
std::uniform_int_distribution<DataType> dist;
24+
for (auto& val : d) { val = dist(rng); }
25+
return d;
26+
}();
27+
return data;
28+
}
29+
30+
// Benchmark Group 1: Single Item, Default "Stalling" Capacity
31+
32+
static void BM_ThisQueue_SingleItem_DefaultBuffer(benchmark::State& state) {
33+
const auto& random_data = get_random_data();
34+
std::vector<DataType> shared_buffer(DefaultQueueCapacity);
35+
LockFreeSpscQueue<DataType> queue(shared_buffer);
36+
37+
std::atomic<bool> verification_failed = false;
38+
std::atomic<bool> consumer_should_stop = false;
39+
std::jthread consumer([&] {
40+
size_t i = 0;
41+
while (!consumer_should_stop.load(std::memory_order_relaxed)) {
42+
auto scope = queue.prepare_read(1);
43+
if (scope.get_items_read() == 1) {
44+
if (scope.get_block1()[0] != random_data[i % RandomDataSize]) verification_failed.store(true);
45+
i++;
46+
} else {
47+
std::this_thread::yield();
48+
}
49+
}
50+
});
51+
52+
size_t total_written = 0;
53+
for (auto _ : state) {
54+
for (size_t n = 0; n < ItemsPerIteration; ++n) {
55+
if (verification_failed.load(std::memory_order_relaxed)) {
56+
state.SkipWithError("Verification failed!"); return;
57+
}
58+
const auto& item_to_write = random_data[total_written % RandomDataSize];
59+
while (true) {
60+
auto scope = queue.prepare_write(1);
61+
if (scope.get_items_written() == 1) {
62+
scope.get_block1()[0] = item_to_write;
63+
break;
64+
}
65+
}
66+
total_written++;
67+
}
68+
}
69+
consumer_should_stop.store(true, std::memory_order_relaxed);
70+
state.SetItemsProcessed(total_written);
71+
}
72+
BENCHMARK(BM_ThisQueue_SingleItem_DefaultBuffer)->Unit(benchmark::kMillisecond)->UseRealTime();
73+
74+
static void BM_Moodycamel_SingleItem_DefaultBuffer(benchmark::State& state) {
75+
const auto& random_data = get_random_data();
76+
moodycamel::ReaderWriterQueue<DataType> queue(DefaultQueueCapacity);
77+
std::atomic<bool> verification_failed = false;
78+
std::atomic<bool> consumer_should_stop = false;
79+
std::jthread consumer([&] {
80+
size_t i = 0;
81+
DataType item;
82+
while (!consumer_should_stop.load(std::memory_order_relaxed)) {
83+
if (queue.try_dequeue(item)) {
84+
if (item != random_data[i % RandomDataSize]) verification_failed.store(true);
85+
i++;
86+
} else {
87+
std::this_thread::yield();
88+
}
89+
}
90+
});
91+
92+
size_t total_written = 0;
93+
for (auto _ : state) {
94+
for (size_t n = 0; n < ItemsPerIteration; ++n) {
95+
if (verification_failed.load(std::memory_order_relaxed)) {
96+
state.SkipWithError("Verification failed!"); return;
97+
}
98+
const auto& item_to_write = random_data[total_written % RandomDataSize];
99+
while (!queue.try_enqueue(item_to_write)) {}
100+
total_written++;
101+
}
102+
}
103+
consumer_should_stop.store(true, std::memory_order_relaxed);
104+
state.SetItemsProcessed(total_written);
105+
}
106+
BENCHMARK(BM_Moodycamel_SingleItem_DefaultBuffer)->Unit(benchmark::kMillisecond)->UseRealTime();
107+
108+
109+
// Benchmark Group 2: Single Item, Large, hopefully "Never-Full" Capacity
110+
111+
static void BM_OurQueue_SingleItem_LargeBuffer(benchmark::State& state) {
112+
const auto& random_data = get_random_data();
113+
std::vector<DataType> shared_buffer(LargeQueueCapacity);
114+
LockFreeSpscQueue<DataType> queue(shared_buffer);
115+
116+
std::atomic<bool> verification_failed = false;
117+
std::atomic<bool> consumer_should_stop = false;
118+
std::jthread consumer([&] {
119+
size_t i = 0;
120+
while (!consumer_should_stop.load(std::memory_order_relaxed)) {
121+
auto scope = queue.prepare_read(1);
122+
if (scope.get_items_read() == 1) {
123+
if (scope.get_block1()[0] != random_data[i % RandomDataSize]) verification_failed.store(true);
124+
i++;
125+
} else {
126+
std::this_thread::yield();
127+
}
128+
}
129+
});
130+
131+
size_t total_written = 0;
132+
for (auto _ : state) {
133+
for (size_t n = 0; n < ItemsPerIteration; ++n) {
134+
if (verification_failed.load(std::memory_order_relaxed)) {
135+
state.SkipWithError("Verification failed!"); return;
136+
}
137+
const auto& item_to_write = random_data[total_written % RandomDataSize];
138+
while (true) {
139+
auto scope = queue.prepare_write(1);
140+
if (scope.get_items_written() == 1) {
141+
scope.get_block1()[0] = item_to_write;
142+
break;
143+
}
144+
// This spin-wait should theoretically never be hit when the buffer is large.
145+
}
146+
total_written++;
147+
}
148+
}
149+
consumer_should_stop.store(true, std::memory_order_relaxed);
150+
state.SetItemsProcessed(total_written);
151+
}
152+
BENCHMARK(BM_OurQueue_SingleItem_LargeBuffer)->Unit(benchmark::kMillisecond)->UseRealTime();
153+
154+
static void BM_Moodycamel_SingleItem_LargeBuffer(benchmark::State& state) {
155+
const auto& random_data = get_random_data();
156+
moodycamel::ReaderWriterQueue<DataType> queue(LargeQueueCapacity);
157+
std::atomic<bool> verification_failed = false;
158+
std::atomic<bool> consumer_should_stop = false;
159+
std::jthread consumer([&] {
160+
size_t i = 0;
161+
DataType item;
162+
while (!consumer_should_stop.load(std::memory_order_relaxed)) {
163+
if (queue.try_dequeue(item)) {
164+
if (item != random_data[i % RandomDataSize]) verification_failed.store(true);
165+
i++;
166+
} else {
167+
std::this_thread::yield();
168+
}
169+
}
170+
});
171+
172+
size_t total_written = 0;
173+
for (auto _ : state) {
174+
for (size_t n = 0; n < ItemsPerIteration; ++n) {
175+
if (verification_failed.load(std::memory_order_relaxed)) {
176+
state.SkipWithError("Verification failed!"); return;
177+
}
178+
const auto& item_to_write = random_data[total_written % RandomDataSize];
179+
while (!queue.try_enqueue(item_to_write)) {
180+
// This spin-wait should theoretically never be hit.
181+
}
182+
total_written++;
183+
}
184+
}
185+
consumer_should_stop.store(true, std::memory_order_relaxed);
186+
state.SetItemsProcessed(total_written);
187+
}
188+
BENCHMARK(BM_Moodycamel_SingleItem_LargeBuffer)->Unit(benchmark::kMillisecond)->UseRealTime();
189+
190+
191+
// Benchmark Group 3: Batch/Bulk Transfers (with Default Capacity)
192+
193+
static void BM_ThisQueue_Batch(benchmark::State& state) {
194+
const size_t batch_size = state.range(0);
195+
const auto& random_data = get_random_data();
196+
std::vector<DataType> shared_buffer(DefaultQueueCapacity);
197+
LockFreeSpscQueue<DataType> queue(shared_buffer);
198+
199+
std::atomic<bool> verification_failed = false;
200+
std::atomic<bool> consumer_should_stop = false;
201+
std::jthread consumer([&]{
202+
size_t received_count = 0;
203+
while (!consumer_should_stop.load(std::memory_order_relaxed)) {
204+
const size_t items_read = queue.try_read(batch_size, [&](auto b1, auto b2){
205+
for (const auto& item : b1) if (item != random_data[(received_count++) % RandomDataSize]) verification_failed.store(true);
206+
for (const auto& item : b2) if (item != random_data[(received_count++) % RandomDataSize]) verification_failed.store(true);
207+
});
208+
if (items_read == 0) std::this_thread::yield();
209+
}
210+
});
211+
212+
size_t total_written = 0;
213+
for (auto _ : state) {
214+
for (size_t n = 0; n < ItemsPerIteration; ) {
215+
if (verification_failed.load(std::memory_order_relaxed)) {
216+
state.SkipWithError("Verification failed!"); consumer_should_stop.store(true); return;
217+
}
218+
219+
const size_t current_rand_idx = total_written % RandomDataSize;
220+
const size_t items_to_rand_end = RandomDataSize - current_rand_idx;
221+
size_t remaining_in_iter = ItemsPerIteration - n;
222+
size_t batch_to_send_size = std::min({batch_size, remaining_in_iter, items_to_rand_end});
223+
224+
std::span<const DataType> sub_batch(&random_data[current_rand_idx], batch_to_send_size);
225+
226+
size_t written_this_batch = 0;
227+
while(written_this_batch < sub_batch.size()) {
228+
written_this_batch += queue.try_write(sub_batch.size() - written_this_batch, [&](auto b1, auto b2){
229+
std::copy_n(sub_batch.begin() + written_this_batch, b1.size(), b1.begin());
230+
if (!b2.empty()) std::copy_n(sub_batch.begin() + written_this_batch + b1.size(), b2.size(), b2.begin());
231+
});
232+
}
233+
n += written_this_batch;
234+
total_written += written_this_batch;
235+
}
236+
}
237+
consumer_should_stop.store(true, std::memory_order_relaxed);
238+
state.SetItemsProcessed(total_written);
239+
}
240+
BENCHMARK(BM_ThisQueue_Batch)->Arg(4)->Arg(16)->Arg(64)->Arg(256)->Unit(benchmark::kMillisecond)->UseRealTime();
241+
242+
243+
static void BM_Moodycamel_Batch(benchmark::State& state) {
244+
const size_t batch_size = state.range(0);
245+
const auto& random_data = get_random_data();
246+
moodycamel::ReaderWriterQueue<DataType> queue(DefaultQueueCapacity);
247+
248+
std::atomic<bool> verification_failed = false;
249+
std::atomic<bool> consumer_should_stop = false;
250+
std::jthread consumer([&]{
251+
size_t received_count = 0;
252+
DataType item;
253+
while (!consumer_should_stop.load(std::memory_order_relaxed)) {
254+
bool dequeued_something = false;
255+
for (size_t i = 0; i < batch_size; ++i) {
256+
if (queue.try_dequeue(item)) {
257+
if (item != random_data[(received_count++) % RandomDataSize]) verification_failed.store(true);
258+
dequeued_something = true;
259+
} else {
260+
break;
261+
}
262+
}
263+
if (!dequeued_something) std::this_thread::yield();
264+
}
265+
});
266+
267+
size_t total_written = 0;
268+
for (auto _ : state) {
269+
for (size_t n = 0; n < ItemsPerIteration; n += batch_size) {
270+
if (verification_failed.load(std::memory_order_relaxed)) {
271+
state.SkipWithError("Verification failed!"); consumer_should_stop.store(true); return;
272+
}
273+
for (size_t i = 0; i < batch_size; ++i) {
274+
const auto& item_to_write = random_data[(total_written + i) % RandomDataSize];
275+
while (!queue.try_enqueue(item_to_write)) {}
276+
}
277+
total_written += batch_size;
278+
}
279+
}
280+
consumer_should_stop.store(true, std::memory_order_relaxed);
281+
state.SetItemsProcessed(total_written);
282+
}
283+
BENCHMARK(BM_Moodycamel_Batch)->Arg(4)->Arg(16)->Arg(64)->Arg(256)->Unit(benchmark::kMillisecond)->UseRealTime();
284+
285+
BENCHMARK_MAIN();

0 commit comments

Comments
 (0)