This document provides a comprehensive overview of the internal architecture, design decisions, and implementation details of the Slick OrderBook library.
Table of Contents
- Overview
- Core Components
- Data Structures
- Memory Management
- Threading Model
- Performance Optimizations
- Event System
- Design Patterns
Overview
Slick OrderBook is designed with three primary goals:
- Ultra-Low Latency: Sub-100ns L2 operations, sub-200ns L3 operations
- Zero Allocations: Object pooling eliminates runtime allocations in hot paths
- Scalability: Efficient management of thousands of symbols
Architecture Principles
- Performance First: Every design decision prioritizes latency and throughput
- Cache Friendly: Data structures optimized for CPU cache locality
- Zero-Cost Abstractions: Template-based design eliminates virtual dispatch overhead
- Type Safety: C++23 concepts provide compile-time type checking
- Single Responsibility: Clear separation of concerns
Core Components
OrderBookL2 - Aggregated Price Levels
Level 2 orderbook maintains aggregated quantities at each price level without tracking individual orders.
Key Features:
- O(log n) add/modify/delete operations via binary search
- O(1) best bid/ask queries via cached TopOfBook
- O(1) side indexing using
Side enum as array index
- Automatic level deletion when quantity becomes zero
Memory Layout:
OrderBookL2 (320 bytes, cache-aligned)
├── SymbolId symbol_id (4 bytes)
├── TopOfBook cached_tob_ (48 bytes)
├── std::array<LevelContainer, 2> books_ (2 × 112 bytes)
│ ├── [0] = Buy side (bids, descending order)
│ └── [1] = Sell side (asks, ascending order)
└── ObserverManager observers_ (variable size)
Hot Path: updateLevel() → getOrCreateLevel() → binary search → notify observers
OrderBookL3 - Order-by-Order Tracking
Level 3 orderbook tracks individual orders with unique IDs, maintaining time priority within each price level.
Key Features:
- O(1) order lookup by ID via hash table
- O(log n) price level operations via binary search
- O(1) insert/remove from priority queue via intrusive linked list
- Automatic L2 aggregation from L3 state
Memory Layout:
OrderBookL3 (384 bytes, cache-aligned)
├── SymbolId symbol_id (4 bytes)
├── TopOfBook cached_tob_ (48 bytes)
├── std::array<PriceLevelMap, SideCount> levels_; (2 × variable)
│ ├── [0] = Buy side price levels
│ └── [1] = Sell side price levels
├── OrderMap order_map_ (hash table for O(1) lookup)
└── ObjectPool<Order> order_pool_ (pre-allocated orders)
Hot Path: addOrModifyOrder() → hash lookup → pool allocate → intrusive list insert
OrderBookManager - Multi-Symbol Coordination
Template-based manager for handling multiple symbols efficiently.
Key Features:
- Thread-safe symbol registry using
shared_mutex
- Per-symbol isolation (no cross-symbol locking)
- Double-checked locking for orderbook creation
- Move semantics for efficient ownership transfer
Thread Safety:
Read operations (getOrderBook):
shared_lock → O(1) hash lookup → return pointer
Write operations (getOrCreateOrderBook):
shared_lock → check existence → if missing:
exclusive_lock → double-check → create → insert
Data Structures
FlatMap - Cache-Friendly Sorted Storage
Alias to std::flat_map (C++23) for storing price levels in sorted order.
Advantages:
- Contiguous memory (excellent cache locality)
- Binary search: O(log n)
- Sequential iteration (no pointer chasing)
- Better performance than
std::map for typical orderbook sizes (<100 levels)
**Trade-offs**:
- Insert/Delete: O(n) due to vector shifts (acceptable for typical use)
- Memory overhead: Minimal compared to node-based containers
**Why Not Custom Implementation?**
- Standard library provides battle-tested implementation
- Compiler optimizations tailored to standard containers
- Reduced maintenance burden
IntrusiveList - Zero-Allocation Order Queue
Doubly-linked list where nodes embed list pointers directly.
**Advantages**:
- O(1) insert/remove operations
- Zero allocations (pointers stored in Order struct)
- Cache-friendly iteration
- Bidirectional traversal
**Structure**:
struct Order {
Order* prev;
Order* next;
};
**Use Case**: Maintaining order priority within a single price level.
ObjectPool - Pre-Allocated Memory
Free-list based object pool for Order structures.
**Key Features**:
- Pre-allocated memory blocks
- O(1) allocate/deallocate
- Exponential growth strategy (64 → 128 → 256 → ... → 8192 per block)
- Cache-aligned allocation via `std::align_val_t`
- RAII lifetime management
**Allocation Strategy**:
Pool State:
├── Free List (linked list of available Order*)
├── Blocks (vector of allocated memory blocks)
└── Stats (allocation count, capacity)
Allocate:
if free_list empty:
grow_pool()
pop from free_list
Deallocate:
push to free_list
**Why ObjectPool?**:
- Eliminates runtime allocations in hot paths
- Predictable performance (no malloc/free overhead)
- Better cache locality (pre-allocated contiguous blocks)
Memory Management
Cache Alignment Strategy
Critical structures are 64-byte aligned to cache line boundaries.
Aligned Structures:
- Order (64 bytes exactly): Perfect 1-cache-line structure
- OrderBookL2 (320 bytes): Prevents false sharing between adjacent orderbooks
- OrderBookL3 (384 bytes): Same rationale as L2
Benefits:
- Eliminates false sharing in multi-symbol scenarios
- Predictable cache behavior
- Better performance in multi-threaded environments
Trade-off:
- Memory overhead: ~40 bytes per OrderBookL2, ~24 bytes per OrderBookL3
- Benefit outweighs cost (measured ~10% improvement in cold cache scenarios)
Memory Footprint
OrderBookL2:
- Base: 320 bytes
- Per level: 24 bytes (PriceLevelL2)
- 100 levels: ~2.7 KB total
OrderBookL3:
- Base: 384 bytes
- Per order: 64 bytes (Order) + hash table entry (~24 bytes)
- 1000 orders: ~88 KB total (within <10KB target per 1000 orders for L3 pool overhead)
Threading Model
Single-Writer, Multiple-Reader (SWMR)
Per-Symbol Isolation:
- Each
OrderBook instance is updated by a single thread (writer)
- No mutex locking within orderbook operations
Thread-Safe Read Operations (use sequence locks):
getTopOfBook() - Full best bid/ask snapshot
getBestBid() - Best bid price level
getBestAsk() - Best ask price level
NOT Thread-Safe (require writer-exclusive access):
getLevels() - Returns vector copy, iterator invalidation risk
getLevel() - Direct pointer to vector element
getLevelByIndex() - Direct pointer to vector element
- All write operations (
updateLevel(), deleteLevel(), etc.)
OrderBookManager Thread Safety:
class OrderBookManager {
mutable std::shared_mutex mutex_;
FlatMap<SymbolId, std::shared_ptr<OrderBook>> symbol_map_;
};
Access Patterns:
- Read (shared lock):
getOrderBook() / getSharedOrderBook() - concurrent reads allowed
- Write (exclusive lock):
getOrCreateOrderBook() / getOrCreateSharedOrderBook() - exclusive access for creation
- Per-Symbol Updates: No cross-symbol locking (each orderbook independent)
- Lifetime: raw pointers are invalidated by
removeOrderBook() / clear(); the *Shared* accessors return a std::shared_ptr that keeps the orderbook alive across concurrent removal
Observer Notifications
void notifyObservers(const Event& event) {
for (const auto& observer : observers_) {
observer->onEvent(event);
}
}
Thread Safety Guarantee:
- Observer registration/removal: Must be done from orderbook's writer thread
- Observer lifetime: Managed by shared_ptr (automatic cleanup)
Performance Optimizations
1. Cached TopOfBook
Problem: Repeatedly computing best bid/ask is expensive.
Solution: Cache TopOfBook and update incrementally.
class OrderBookL2 {
TopOfBook cached_tob_;
void updateLevel(...) {
}
};
Benefit: Best bid/ask queries are 0.25ns (40x faster than target).
2. Array Indexing by Side
Problem: Storing bid/ask books separately requires branching.
Solution: Use Side enum (0=Buy, 1=Sell) as array index.
enum Side : uint8_t { Buy = 0, Sell = 1 };
std::array<LevelContainer, 2> books_;
Benefit: Eliminates branch mispredictions, enables compiler optimizations.
3. Quantity=0 Deletion
Problem: Explicit "action" enums (Add/Modify/Delete) complicate API.
Solution: Quantity=0 implies deletion.
void updateLevel(Side side, Price price, Quantity quantity, ...) {
if (quantity == 0) {
deleteLevel(side, price);
} else {
}
}
Benefit: Simpler API, reduced event payload size, fewer branches.
4. Return Pairs for Efficiency
Problem: Calculating level index multiple times is wasteful.
Solution: Return std::pair<Level*, uint16_t> from helpers.
auto [level, level_idx] = getOrCreateLevel(...);
notifyObservers(..., level_idx);
Benefit: Single calculation, clean syntax via structured bindings.
5. Zero-Copy Iteration
Problem: Returning std::vector<Level> copies data.
Solution: Return const std::span<const Level> or const reference.
const LevelContainer& getLevels(Side side) const {
return books_[side];
}
Benefit: No allocations, no copies, just a pointer/size pair.
Event System
Event Types
- PriceLevelUpdate: L2 level changes
level_index (uint16_t): 0-based position (0 = best)
change_flags (uint8_t): PriceChanged | QuantityChanged
- Helper:
isTopN(n) for efficient top-N filtering
- OrderUpdate: L3 individual order changes
price_level_index (uint16_t): Index of parent price level
priority (uint64_t): Order priority for queue position
change_flags (uint8_t): Same as PriceLevelUpdate
- Trade: Executed trades (future extension)
- TopOfBook: Best bid/ask snapshot
Observer Interface
class IOrderBookObserver {
virtual void onPriceLevelUpdate(const PriceLevelUpdate&) = 0;
virtual void onOrderUpdate(const OrderUpdate&) = 0;
virtual void onTrade(const Trade&) = 0;
virtual void onTopOfBookUpdate(const TopOfBook&) = 0;
virtual void onSnapshotBegin(SymbolId, SeqNum, Timestamp) = 0;
virtual void onSnapshotEnd(SymbolId, SeqNum, Timestamp) = 0;
};
Snapshot Callbacks:
onSnapshotBegin: Called before processing initial orderbook snapshot
onSnapshotEnd: Called after snapshot completes
- Use case: Suppress UI updates during snapshot load
Design Patterns
1. Template-Based Polymorphism (CRTP)
Why: Eliminate virtual dispatch overhead in hot paths.
template<typename OrderBookType>
class OrderBookManager {
FlatMap<SymbolId, std::shared_ptr<OrderBookType>> symbol_map_;
};
OrderBookManager<OrderBookL2> l2_manager;
OrderBookManager<OrderBookL3> l3_manager;
Benefit: Zero-cost abstraction, full inlining.
2. Policy-Based Design
Why: Compile-time customization without runtime overhead.
template<ComparatorPolicy Comp>
class LevelContainer {
std::flat_map<Price, Level, Comp> levels_;
};
struct BidComparator { };
struct AskComparator { };
Benefit: Single codebase, multiple behaviors, zero overhead.
3. Observer Pattern (Non-Virtual)
Why: Lock-free notifications, flexible observation.
std::vector<std::shared_ptr<IOrderBookObserver>> observers_;
void notify(const Event& event) {
for (auto& obs : observers_) {
obs->onEvent(event);
}
}
Trade-off: Virtual dispatch for observers (acceptable since notification is outside critical path).
4. RAII for Resource Management
Why: Automatic cleanup, exception safety.
class OrderBookL3 {
ObjectPool<Order> order_pool_;
~OrderBookL3() {
}
};
Benefit: No manual memory management, leak-proof.
Build Modes
Compiled Library (Default)
Advantages:
- Fast compile times (compile once, link many)
- Stable ABI for shared libraries
- Smaller binary sizes
- Easier debugging
How It Works:
- Public headers in
include/slick/orderbook/
- Implementation in
src/
- Explicit template instantiations in
src/instantiations/
Header-Only Mode
Advantages:
- Maximum inlining opportunities
- No linking step
- Easier integration
How It Works:
- Define
SLICK_ORDERBOOK_HEADER_ONLY
- Implementations included from
include/slick/orderbook/detail/impl/
Trade-off: Longer compile times, larger binaries.
Performance Profile
Measured Results
| Metric | Target | Actual | Factor |
| L2 Add/Modify | <100ns | 21-33ns | 3-5x faster |
| L3 Add/Modify | <200ns | 59-490ns | 2-3x faster |
| Best Bid/Ask | <10ns | 0.25ns | 40x faster |
| Observer | <50ns | 2-3ns | 16-25x faster |
Hot Spots (from profiling)
- **
std::lower_bound**: Binary search in FlatMap (expected, optimal for <100 levels)
- **`updateLevel`**: Main update entry point (inline-optimized)
- **`getBestBid`/`getBestAsk`**: Cached access (0.25ns)
- **Benchmark framework overhead**: Google Benchmark iteration overhead
Cache Behavior
- **94.6% kernel time** in profiler indicates excellent cache utilization
- Code executes so fast that most time is OS scheduling
- No cache thrashing detected
- False sharing prevented by cache alignment
Future Considerations
Potential Enhancements
- SIMD for Observer Iteration: Vectorize observer notification loop
- Prefetch Hints: Prefetch next price level during search
- Custom Allocator: Fine-tune ObjectPool growth strategy
- Lock-Free Reads: Sequence locks for concurrent readers
- Batch Processing: Amortize overhead across multiple updates
Not Planned (Premature Optimization)
- Assembly-Level Tuning: Current performance exceeds targets by 2-40x
- Custom Binary Search:
std::lower_bound is already optimal
- Hand-Rolled Allocators: ObjectPool meets all needs
Conclusion
Slick OrderBook achieves its performance goals through:
- Cache-Friendly Data Structures: FlatMap, IntrusiveList, cache alignment
- Zero Allocations: ObjectPool, intrusive design
- Template-Based Design: Zero-cost abstractions
- Intelligent Caching: TopOfBook, level indices
- Lock-Free Design: Single-writer per symbol
The architecture prioritizes latency and throughput while maintaining clean, maintainable code.
For more details, see:
- Profiling Results - Performance analysis
- Cache Alignment Results - Optimization details