Circular Buffer Wait Queue Implementation
Quick Summary
Circular queue is now DR_EVT’s default FCFS wait-queue implementation.
It’s structurally identical to the deque-based FCFSScheduler it replaced
as default - same job entry layout, same lazy-mark-and-compact removal,
same pop_front() head consumption, same indexed backfill scan - but
backed by boost::circular_buffer instead of std::deque. Measured
~28% faster than deque on average (10K jobs, 500 nodes).
Command: ./simulator trace.csv (default) or explicitly ./simulator trace.csv --queue_impl circular
Performance Results (10K jobs, 500 nodes, average of 3 trials)
500 nodes is the tightest node count that keeps every job in this trace schedulable (the trace’s largest single job requests exactly 500 nodes), so this reflects real scheduling contention rather than a mostly-idle cluster - a materially different (and more realistic) scenario than an earlier 2,000-node version of this benchmark.
Implementation |
Time (s) |
vs Deque |
|---|---|---|
Deque |
0.812 |
1.00x (baseline) |
Block-16 |
0.588 |
0.72x (27.6% faster) |
Circular |
0.580 |
0.71x (28.5% faster) |
At this tighter node count, circular and block-16 are close (circular only ~1% faster than block-16 here) - a smaller margin than the ~24% gap measured at 2,000 nodes. Run-to-run variance in this measurement is real and non-trivial (observed single trials ranging from -12% to -73% relative to deque across repeated runs); treat the specific percentages above as directionally representative, not precise.
✅ Produces byte-for-byte identical output to deque, in every one of the 34
comprehensive differential test cases (tests/test_fcfs_comprehensive.sh),
and separately confirmed on the 10K-job benchmark trace itself.
Exact numbers will vary by hardware; re-run tests/benchmark_block_sizes.sh
(which also benchmarks circular alongside every block size) to measure on
your own machine.
Why It’s Faster Than Deque
boost::circular_buffer stores its elements in one contiguous block of
memory. std::deque is typically implemented as a sequence of fixed-size
chunks - indexed access (operator[], used throughout the backfill scan)
requires a division/modulo to find the right chunk, then an offset within
it. A circular buffer’s indexed access is a single, direct offset into that
one contiguous block. Both containers give O(1) push_back/pop_front, so
this difference in indexed-access cost is the main source of the speedup.
Why It’s Faster Than Block Queue
Block queue’s overhead is dominated by maintaining two red-black trees per
block (see BLOCK_QUEUE.md: ~70% of its overhead). Circular queue has
no per-block bookkeeping at all - it’s structurally the same linear scan
deque already does, just over faster-to-index storage.
The Fixed-Capacity Trade-off
Unlike std::deque, boost::circular_buffer has a capacity fixed at
construction. A push_back() on a full buffer overwrites the oldest
element rather than growing - silently dropping a job would be a serious
correctness bug, so this needed explicit handling:
Default capacity (0): sized to the job trace’s own length at construction. Since
insert_job()is called at most once per entry in the trace over the scheduler’s lifetime (seeSimulation::submit_job()), this guarantees the buffer can never overflow - the same “always correct, uses the worst-case amount of memory upfront” trade-off as sizing any fixed buffer to the largest possible input.--wait_queue_capacity SIZE: an explicit, smaller capacity, trading that guarantee for a smaller initial allocation.--wait_queue_overflow {abort|grow}: what happens if an insert exceeds the capacity actually chosen.abortthrowsstd::runtime_error, caught by the top-level handler (or reported back to the gRPC client), ending the simulation cleanly with exit code 1.grow(default) reallocates to double the current capacity viaboost::circular_buffer::set_capacity(), which preserves every existing entry - confirmed directly against Boost’s own documentation and behavior:set_capacity()only drops elements when shrinking below the current size, never when growing.
Testing
Quick Test
# Correctness test (deque vs multimap vs block vs circular)
./tests/test_fcfs_comprehensive.sh --correctness
# Performance benchmark (10K jobs, includes circular alongside all block sizes)
./tests/benchmark_block_sizes.sh
Manually Testing Capacity/Overflow
# Force growth: capacity 10 is far smaller than most real traces
${CMAKE_INSTALL_PREFIX}/bin/simulator trace.csv --priority_policy fcfs --queue_impl circular \
--wait_queue_capacity 10 --wait_queue_overflow grow
# Force a clean abort instead
${CMAKE_INSTALL_PREFIX}/bin/simulator trace.csv --priority_policy fcfs --queue_impl circular \
--wait_queue_capacity 10 --wait_queue_overflow abort
Usage
Command-Line Options
# Default (deque)
${CMAKE_INSTALL_PREFIX}/bin/simulator trace.csv --priority_policy fcfs
# Circular queue, default capacity (sized to the job trace)
${CMAKE_INSTALL_PREFIX}/bin/simulator trace.csv --priority_policy fcfs --queue_impl circular
# Circular queue with an explicit capacity and abort-on-overflow
${CMAKE_INSTALL_PREFIX}/bin/simulator trace.csv --priority_policy fcfs --queue_impl circular \
--wait_queue_capacity 500 --wait_queue_overflow abort
Factory Pattern
std::unique_ptr<SchedulerBase> scheduler = create_scheduler(
total_nodes,
job_data,
BackfillPolicy::EASY,
PriorityPolicy::FCFS,
DurationEstimateMode::USE_LIMIT,
QueueImplementation::CIRCULAR,
128, // block_size (unused for circular)
0, // wait_queue_capacity: 0 = size of job_data
CircularOverflowPolicy::GROW
);
Also Reachable via Protobuf
Both protobuf-based configuration paths support queue_impl,
wait_queue_capacity, and wait_queue_overflow alongside their existing
block_size field:
The gRPC service’s
InitRequestmessage (src/proto/dr_evt_service.proto)The
.prototextconfig file format read via--config(src/proto/dr_evt_params.proto)
An empty queue_impl string, empty wait_queue_overflow string, or a 0
wait_queue_capacity keeps Sim_Params’ own defaults (circular; grow;
sized to the job trace) in both paths.
Recommendations
Production
✅ circular is the default - no action needed for most FCFS workloads; measured faster than both deque and block queue.
✅ Use --queue_impl deque if you’d rather not depend on Boost, or want
the simplest, most battle-tested option; the performance difference
measured here is real but not dramatic.
Research/Testing
✅ Use the default capacity (0) unless you specifically want to test the
overflow/grow path or bound memory usage - a wrong guess at capacity with
--wait_queue_overflow grow costs nothing but a doubling reallocation partway
through the run.
Files
Core Implementation
src/sim/scheduler_circular_fcfs.{hpp,cpp}- the scheduler itselfsrc/sim/scheduler_base.cpp- factory functionsrc/params/sim_params.{hpp,cpp}- CLI parsing,QueueImplementation::CIRCULAR,CircularOverflowPolicy
Tests
tests/test_fcfs_comprehensive.sh- correctness verification (deque vs multimap vs block vs circular)tests/benchmark_block_sizes.sh- performance comparison (all block sizes plus circular)
Protobuf Wiring
src/proto/dr_evt_service.proto,src/proto/dr_evt_server.cpp- gRPC service pathsrc/proto/dr_evt_params.proto,src/proto/dr_evt_params.cpp-.prototextconfig file path
References
Boost Circular Buffer: https://www.boost.org/doc/libs/release/libs/circular_buffer/
BLOCK_QUEUE.md- the queue implementation this one was written to outperform, and the source of the multi-index overhead analysis referenced above