@@ -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
9397const 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
113119const 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
133141const 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
173185int 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
193207enum queue_id_t
@@ -248,14 +262,14 @@ const int QUEUE_MAX_THREADS[QUEUE_COUNT] = {
248262};
249263
250264const 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
444622template <typename TQueue, typename item_t >
445623double 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