Skip to content

Commit d655418

Browse files
authored
Merge pull request #450 from benkier0/benchmarks/dedicated-consumer-dequeue
benchmarks: add dedicated-consumer benchmarks for try_dequeue_from_producer
2 parents 9afb997 + 16cf73d commit d655418

1 file changed

Lines changed: 198 additions & 13 deletions

File tree

benchmarks/benchmarks.cpp

Lines changed: 198 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ enum benchmark_type_t
6666
bench_empty_dequeue,
6767
bench_enqueue_dequeue_pairs,
6868
bench_heavy_concurrent,
69-
69+
bench_dedicated_consumer,
70+
bench_dedicated_consumer_bulk,
71+
7072
BENCHMARK_TYPE_COUNT
7173
};
7274

@@ -87,7 +89,9 @@ const char BENCHMARK_SHORT_NAMES[BENCHMARK_TYPE_COUNT][32] = {
8789
"mpsc",
8890
"empty_dequeue",
8991
"enqueue_dequeue_pairs",
90-
"heavy_concurrent"
92+
"heavy_concurrent",
93+
"dedicated_consumer",
94+
"dedicated_consumer_bulk"
9195
};
9296

9397
const char BENCHMARK_NAMES[BENCHMARK_TYPE_COUNT][64] = {
@@ -107,7 +111,9 @@ const char BENCHMARK_NAMES[BENCHMARK_TYPE_COUNT][64] = {
107111
"multi-producer, single-consumer",
108112
"dequeue from empty",
109113
"enqueue-dequeue pairs",
110-
"heavy concurrent"
114+
"heavy concurrent",
115+
"dedicated consumer (try_dequeue_from_producer)",
116+
"dedicated consumer bulk (try_dequeue_bulk_from_producer)"
111117
};
112118

113119
const char BENCHMARK_DESCS[BENCHMARK_TYPE_COUNT][256] = {
@@ -127,7 +133,9 @@ const char BENCHMARK_DESCS[BENCHMARK_TYPE_COUNT][256] = {
127133
"Measures the average speed of dequeueing with only one consumer, but multiple producers",
128134
"Measures the average speed of attempting to dequeue from an empty queue\n (that eight separate threads had at one point enqueued to)",
129135
"Measures the average operation speed with each thread doing an enqueue\n followed by a dequeue",
130-
"Measures the average operation speed with many threads under heavy load"
136+
"Measures the average operation speed with many threads under heavy load",
137+
"Measures dequeue speed when each consumer is paired 1:1 with a producer via\n try_dequeue_from_producer, bypassing the full producer-list scan.\n 'Without tokens' uses general try_dequeue as a baseline for comparison.",
138+
"Measures bulk dequeue speed when each consumer is paired 1:1 with a producer\n via try_dequeue_bulk_from_producer.\n 'Without tokens' uses general try_dequeue_bulk as a baseline for comparison."
131139
};
132140

133141
const char BENCHMARK_SINGLE_THREAD_NOTES[BENCHMARK_TYPE_COUNT][256] = {
@@ -147,6 +155,8 @@ const char BENCHMARK_SINGLE_THREAD_NOTES[BENCHMARK_TYPE_COUNT][256] = {
147155
"",
148156
"No contention -- measures raw failed dequeue speed on empty queue",
149157
"No contention -- measures speed of immediately dequeueing the item that was just enqueued",
158+
"",
159+
"",
150160
""
151161
};
152162

@@ -168,6 +178,8 @@ int BENCHMARK_THREADS_MEASURED[BENCHMARK_TYPE_COUNT] = {
168178
0,
169179
0,
170180
0,
181+
0,
182+
0,
171183
};
172184

173185
int BENCHMARK_THREADS[BENCHMARK_TYPE_COUNT][9] = {
@@ -188,6 +200,8 @@ int BENCHMARK_THREADS[BENCHMARK_TYPE_COUNT][9] = {
188200
{ 1, 2, 8, 32, 0, 0, 0, 0, 0 },
189201
{ 1, 2, 4, 8, 32, 0, 0, 0, 0 },
190202
{ 2, 3, 4, 8, 12, 16, 32, 48, 0 },
203+
{ 2, 4, 8, 16, 32, 0, 0, 0, 0 },
204+
{ 2, 4, 8, 16, 32, 0, 0, 0, 0 },
191205
};
192206

193207
enum queue_id_t
@@ -248,14 +262,14 @@ const int QUEUE_MAX_THREADS[QUEUE_COUNT] = {
248262
};
249263

250264
const bool QUEUE_BENCH_SUPPORT[QUEUE_COUNT][BENCHMARK_TYPE_COUNT] = {
251-
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
252-
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
253-
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1 },
254-
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1 },
255-
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1 },
256-
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1 },
257-
{ 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0 },
258-
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1 }
265+
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
266+
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
267+
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0 },
268+
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0 },
269+
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0 },
270+
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0 },
271+
{ 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
272+
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0 }
259273
};
260274

261275

@@ -433,13 +447,177 @@ counter_t determineMaxOpsForBenchmark(benchmark_type_t benchmark, int nthreads,
433447
}), nthreads);
434448
}
435449

450+
case bench_dedicated_consumer:
451+
case bench_dedicated_consumer_bulk: {
452+
// Proxy timing using single-threaded pre-fill + dequeue; actual benchmark
453+
// uses try_dequeue_from_producer but only runs for ConcurrentQueue.
454+
return adjustForThreads(rampUpToMeasurableNumberOfMaxOps([](counter_t ops) {
455+
TQueue q;
456+
item_t item = 1;
457+
for (counter_t i = 0; i != ops; ++i) {
458+
q.enqueue(item);
459+
}
460+
item_t item_rec;
461+
auto start = getSystemTime();
462+
for (counter_t i = 0; i != ops; ++i) {
463+
q.try_dequeue(item_rec);
464+
}
465+
return getTimeDelta(start);
466+
}), nthreads);
467+
}
468+
436469
default:
437470
assert(false && "Every benchmark type must be handled here!");
438471
return 0;
439472
}
440473
}
441474

442475

476+
// Base template: no-op stub so that runBenchmark<TQueue> compiles for all
477+
// queue types even though try_dequeue_from_producer only exists on
478+
// moodycamel::ConcurrentQueue. The real work is in the specialisation below.
479+
template<typename TQueue, typename item_t>
480+
struct DedicatedConsumerBenchmark {
481+
static double run(benchmark_type_t, int, bool, counter_t, counter_t&)
482+
{
483+
assert(false && "bench_dedicated_consumer* is only supported for moodycamel::ConcurrentQueue");
484+
return 0;
485+
}
486+
};
487+
488+
template<>
489+
struct DedicatedConsumerBenchmark<moodycamel::ConcurrentQueue<int, Traits>, int>
490+
{
491+
static double run(benchmark_type_t benchmark, int nthreads, bool useTokens, counter_t maxOps, counter_t& out_opCount)
492+
{
493+
using Queue = moodycamel::ConcurrentQueue<int, Traits>;
494+
bool isBulk = (benchmark == bench_dedicated_consumer_bulk);
495+
int numPairs = nthreads / 2;
496+
Queue q;
497+
498+
// All producer tokens are created before threads start to avoid
499+
// construction races. Tokens are move-constructible; reserve() ensures
500+
// emplace_back never reallocates, keeping element addresses stable.
501+
std::vector<Queue::producer_token_t> prodToks;
502+
prodToks.reserve(numPairs);
503+
for (int i = 0; i < numPairs; ++i)
504+
prodToks.emplace_back(q);
505+
506+
std::vector<SimpleThread> threads(nthreads);
507+
std::vector<double> timings(nthreads, 0.0);
508+
std::vector<counter_t> consumerOps(numPairs, 0);
509+
std::atomic<int> ready(0);
510+
511+
if (isBulk) {
512+
std::vector<int> enqData(BULK_BATCH_SIZE);
513+
for (counter_t i = 0; i != BULK_BATCH_SIZE; ++i)
514+
enqData[i] = static_cast<int>(i);
515+
counter_t itemsPerPair = maxOps * BULK_BATCH_SIZE;
516+
517+
for (int tid = 0; tid != numPairs; ++tid) {
518+
threads[tid] = SimpleThread([&](int id) {
519+
ready.fetch_add(1, std::memory_order_relaxed);
520+
while (ready.load(std::memory_order_relaxed) != nthreads)
521+
continue;
522+
auto start = getSystemTime();
523+
for (counter_t i = 0; i != maxOps; ++i)
524+
q.enqueue_bulk(prodToks[id], enqData.cbegin(), enqData.size());
525+
timings[id] = getTimeDelta(start);
526+
}, tid);
527+
}
528+
for (int tid = numPairs; tid != nthreads; ++tid) {
529+
threads[tid] = SimpleThread([&](int id) {
530+
int pairId = id - numPairs;
531+
std::vector<int> items(BULK_BATCH_SIZE);
532+
ready.fetch_add(1, std::memory_order_relaxed);
533+
while (ready.load(std::memory_order_relaxed) != nthreads)
534+
continue;
535+
counter_t totalDequeued = 0;
536+
counter_t totalOps = 0;
537+
auto start = getSystemTime();
538+
if (useTokens) {
539+
while (totalDequeued < itemsPerPair) {
540+
auto actual = q.try_dequeue_bulk_from_producer(prodToks[pairId], items.begin(), items.size());
541+
totalDequeued += actual;
542+
totalOps += actual + (actual == items.size() ? 0 : 1);
543+
}
544+
} else {
545+
while (totalDequeued < itemsPerPair) {
546+
auto actual = q.try_dequeue_bulk(items.begin(), items.size());
547+
totalDequeued += actual;
548+
totalOps += actual + (actual == items.size() ? 0 : 1);
549+
}
550+
}
551+
timings[id] = getTimeDelta(start);
552+
consumerOps[pairId] = totalOps;
553+
}, tid);
554+
}
555+
556+
double result = 0;
557+
out_opCount = 0;
558+
for (int tid = 0; tid != nthreads; ++tid) {
559+
threads[tid].join();
560+
result += timings[tid];
561+
}
562+
for (int p = 0; p < numPairs; ++p)
563+
out_opCount += itemsPerPair + consumerOps[p];
564+
return result;
565+
566+
} else {
567+
for (int tid = 0; tid != numPairs; ++tid) {
568+
threads[tid] = SimpleThread([&](int id) {
569+
ready.fetch_add(1, std::memory_order_relaxed);
570+
while (ready.load(std::memory_order_relaxed) != nthreads)
571+
continue;
572+
int x = 1;
573+
auto start = getSystemTime();
574+
for (counter_t i = 0; i != maxOps; ++i)
575+
q.enqueue(prodToks[id], x);
576+
timings[id] = getTimeDelta(start);
577+
}, tid);
578+
}
579+
for (int tid = numPairs; tid != nthreads; ++tid) {
580+
threads[tid] = SimpleThread([&](int id) {
581+
int pairId = id - numPairs;
582+
ready.fetch_add(1, std::memory_order_relaxed);
583+
while (ready.load(std::memory_order_relaxed) != nthreads)
584+
continue;
585+
int item_rec;
586+
counter_t dequeued = 0;
587+
counter_t attempts = 0;
588+
auto start = getSystemTime();
589+
if (useTokens) {
590+
while (dequeued < maxOps) {
591+
if (q.try_dequeue_from_producer(prodToks[pairId], item_rec))
592+
++dequeued;
593+
++attempts;
594+
}
595+
} else {
596+
while (dequeued < maxOps) {
597+
if (q.try_dequeue(item_rec))
598+
++dequeued;
599+
++attempts;
600+
}
601+
}
602+
timings[id] = getTimeDelta(start);
603+
consumerOps[pairId] = attempts;
604+
}, tid);
605+
}
606+
607+
double result = 0;
608+
out_opCount = 0;
609+
for (int tid = 0; tid != nthreads; ++tid) {
610+
threads[tid].join();
611+
result += timings[tid];
612+
}
613+
for (int p = 0; p < numPairs; ++p)
614+
out_opCount += maxOps + consumerOps[p];
615+
return result;
616+
}
617+
}
618+
};
619+
620+
443621
// Returns time elapsed, in (fractional) milliseconds
444622
template<typename TQueue, typename item_t>
445623
double runBenchmark(benchmark_type_t benchmark, int nthreads, bool useTokens, unsigned int randSeed, counter_t maxOps, int maxThreads, counter_t& out_opCount)
@@ -1646,12 +1824,19 @@ double runBenchmark(benchmark_type_t benchmark, int nthreads, bool useTokens, un
16461824
break;
16471825
}
16481826

1827+
case bench_dedicated_consumer:
1828+
case bench_dedicated_consumer_bulk: {
1829+
result = DedicatedConsumerBenchmark<TQueue, item_t>::run(benchmark, nthreads, useTokens, maxOps, out_opCount);
1830+
forceNoOptimizeDummy = 0;
1831+
break;
1832+
}
1833+
16491834
default:
16501835
assert(false && "Every benchmark type must be handled here!");
16511836
result = 0;
16521837
out_opCount = 0;
16531838
}
1654-
1839+
16551840
(void)forceNoOptimizeDummy;
16561841

16571842
return result;

0 commit comments

Comments
 (0)