perf: O(1) tick-indexed array + bitmap price levels (replaces rbtree) - #34
Merged
Conversation
Benchmark PRThroughput on hosted runners is noisy and not performance-calibrated; treat the numbers as informational only. Realistic deep-book throughput (W=50000)
Matching-engine-benchmark (perf mode)Harness pinned at
|
…ee optional Replace the per-side boost::intrusive::rbtree<OrderQueue> price-level container with a compile-time policy. The default ArrayLevels<P> is a flat tick-indexed std::vector<OrderQueue*> + a 3-level occupancy bitmap (clz/ctz) giving O(1) level find/create/erase and best-bid/ask. RbTreeLevels<P> (the original rbtree) remains selectable via OrderBook<Notification, RbTreeLevels>. Selection is a template-template param — zero runtime cost, no virtual dispatch; both backends inline under LTO. PriceLevel<P, Store=ArrayLevels<P>> keeps volume/order accounting and the matching loops COMMON across backends. Motivation: in the worst-case matching scenarios 97-99.8% of price levels hold a single sub-millisecond order, yet the rbtree paid an O(log N) node insert + erase/rebalance (pointer-chasing cache-cold nodes) per add/cancel (~34% of steady-state matching). The array makes that O(1) and cache-local. ArrayLevels grows on demand (never worse than the rbtree on any input); good initial sizing means typical workloads never grow. base=0. Worst-case engine throughput (deterministic single-thread replay): slowest scenario (swing-25) 27.3 -> 34.4 M msgs/s, +23.7%; every scenario +19-27%. The template policy is zero-cost (array default reproduces the pre-policy number; rbtree backend reproduces the original within ~1%). Byte-identical trade streams on all 5 harness scenarios; full suite passes for BOTH backends (orderbook 52, determinism 4, pricelevel) in Release and Debug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0196EKKWudtsg3gButufhDxz
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the per-side
boost::intrusive::rbtree<OrderQueue>price-level container with a flat tick-indexedstd::vector<OrderQueue*>+ a 3-level occupancy bitmap (clz/ctz) for O(1) level find/create/erase and best-bid/ask.Motivation (worst-case matching)
Profiling the worst-case scenarios (swing-25, flash-crash) showed 97–99.8% of price levels hold a single, sub-millisecond order, yet every add created an rbtree node and every cancel/fill did an O(log N) erase + rebalance, pointer-chasing scattered, cache-cold nodes — ~34% of steady-state matching time. Array indexing makes those operations O(1) and cache-friendly.
Design
levels_: tick-indexedvector<OrderQueue*>(nullptr= empty); index =(price.fp - base)/tick_fp,base = 0so all positive-price ticks are non-negative.highestSet/lowestSet) and level advance (largestLessThan/smallestGreaterThan).cap = max(t+1, 2*size)rounded to 64, set bits preserved) rather than failing — so it's never worse than the rbtree on any input. Good initial sizing means typical workloads never grow.OrderQueue/Orderand the OrderQueue pool are unchanged.Results (this PR's increment over current
main, which already has funcref + LTO)Worst-case engine throughput via a deterministic single-threaded replay (the threaded harness is too bimodal on shared hardware to read cleanly):
Worst-case scenario is the same one (swing-25) before and after, and its floor rises +23%; every scenario improves; nothing regresses.
Correctness
🤖 Generated with Claude Code
https://claude.ai/code/session_0196EKKWudtsg3gButufhDxz