This guide provides best practices and recommendations for achieving optimal performance with Slick OrderBook.
Table of Contents
- Performance Characteristics
- Best Practices
- Common Pitfalls
- Optimization Techniques
- Benchmarking
- Profiling
Performance Characteristics
Measured Performance (p99 Latency)
All measurements on modern x86-64 CPU (3.0+ GHz):
| Operation | Latency | Throughput | Complexity |
| Level 2 | | | |
| Add new level | 21-28ns | 36-48M ops/sec | O(log n) |
| Modify level | 24-31ns | 32-42M ops/sec | O(log n) |
| Delete level | 21-33ns | 30-48M ops/sec | O(log n) |
| Get best bid/ask | 0.25-0.33ns | 3-4 billion ops/sec | O(1) |
| Get all levels | ~15ns | 67M ops/sec | O(n) |
| Level 3 | | | |
| Add/modify order | 59-84ns | 12-17M ops/sec | O(log n) + O(1) |
| Delete order | 59-490ns | 2-17M ops/sec | O(1) hash |
| Execute order | ~59ns | 17M ops/sec | O(1) |
| Observer | | | |
| Per-observer notification | 2-3ns | 333-500M/sec | O(1) |
| Multi-Symbol | | | |
| 10 symbols | 7.7M items/s | - | - |
| 100 symbols | 5.5M items/s | - | - |
| 1000 symbols | 2.7M items/s | - | - |
Memory Footprint
| Configuration | Memory Usage |
| OrderBookL2 base | 320 bytes |
| OrderBookL2 + 100 levels | ~2.7 KB |
| OrderBookL3 base | 384 bytes |
| OrderBookL3 + 1000 orders | ~88 KB |
| 1000 L2 symbols | ~320 KB base + level data |
Best Practices
1. Choose the Right Orderbook Type
Use OrderBookL2 when:
- You only need aggregated price levels
- Individual order tracking is not required
- Minimizing memory footprint is important
- Maximum performance is critical
Use OrderBookL3 when:
- You need full order-by-order visibility
- Order ID tracking is required
- You need to aggregate L2 from L3 data
- You're building a matching engine
Performance Difference:
- L2 is ~2-3x faster than L3 for updates
- L2 uses ~30x less memory than L3 (for 1000 orders)
2. Pre-Allocate When Possible
ObjectPool Pre-Allocation:
OrderBookL3 book(symbol_id);
book.reserve(1000);
Benefit: Eliminates allocation overhead during hot path.
3. Use Batch Updates for Snapshots
Don't:
for (const auto& level : snapshot) {
book.updateLevel(level.side, level.price, level.qty, ts, seq);
}
Do:
book.emitSnapshot(seq_num, timestamp);
Benefit: Reduces observer overhead from O(n) notifications to O(1).
4. Filter Events at the Observer
Efficient Top-N Filtering:
class TopNObserver : public IOrderBookObserver {
void onPriceLevelUpdate(const PriceLevelUpdate& update) override {
if (update.isTopN(10)) {
processUpdate(update);
}
}
};
Benefit: Reduces processing overhead for deep orderbooks.
5. Minimize Observer Count
Each observer adds ~2-3ns per event:
- 1 observer: ~2ns overhead
- 10 observers: ~20ns overhead
- 100 observers: ~200ns overhead
Recommendation: Aggregate multiple consumers into a single observer when possible.
6. Use Const References
Don't:
std::vector<PriceLevelL2> levels = book.getLevelsL2(Side::Buy);
Do:
const auto& levels = book.getLevels(Side::Buy);
for (const auto& level : levels) {
}
Benefit: Eliminates unnecessary copies.
7. Cache TopOfBook Queries
If querying multiple times per update:
const TopOfBook tob = book.getTopOfBook();
Price spread = tob.getSpread();
Price mid = tob.getMidPrice();
Note: getTopOfBook() is already O(1) (0.25ns), but avoiding multiple calls helps.
8. Use Fixed-Point Arithmetic
Prices are stored as int64_t:
Price price = 100.2550 * 10000;
book.updateLevel(Side::Buy, price, quantity, ts, seq);
double display_price = static_cast<double>(price) / 10000.0;
Benefit: Avoids floating-point precision issues and is faster.
9. Thread Affinity for Critical Paths
Pin orderbook update thread to dedicated CPU core:
#include <pthread.h>
void pinToCPU(int cpu_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
}
pinToCPU(2);
Benefit: Reduces cache misses and context switch overhead.
10. Use Release Builds
Always benchmark in Release mode:
cmake -DCMAKE_BUILD_TYPE=Release ..
Performance difference: Debug builds can be 10-100x slower!
Common Pitfalls
❌ Pitfall 1: Copying Orderbooks
OrderBookL2 book1(1);
OrderBookL2 book2 = book1;
Why it's bad: Copies all internal state (price levels, observers, pools).
Solution: Use pointers or references:
OrderBookL2* book_ptr = manager.getOrderBook(1);
❌ Pitfall 2: Excessive Observer Notifications
for (int i = 0; i < 1000; ++i) {
book.updateLevel(Side::Buy, base_price - i, qty, ts, seq);
}
Why it's bad: O(n × m) where n = updates, m = observers.
Solution: Batch updates or use snapshot mode.
❌ Pitfall 3: Unnecessary Conversions
for (...) {
auto levels = l3_book.getLevelsL2(Side::Buy);
}
Why it's bad: L3→L2 aggregation has O(n) cost.
Solution: Cache the result if querying multiple times:
const auto& levels = l3_book.getLevelsL2(Side::Buy);
❌ Pitfall 4: Not Using OrderBookManager
std::map<SymbolId, OrderBookL2> books;
Why it's bad: No thread safety, manual memory management, no optimization.
Solution: Use OrderBookManager:
OrderBookManager<OrderBookL2> manager;
auto* book = manager.getOrCreateOrderBook(symbol_id);
❌ Pitfall 5: Debug Assertions in Production
Why it's bad: Assertions add overhead in hot paths.
Solution: Always build production with Release mode (-DNDEBUG).
Optimization Techniques
Technique 1: Compiler Optimizations
Enable maximum optimizations:
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG")
Flags explained:
-O3: Aggressive optimizations
-march=native: CPU-specific instructions (AVX2, etc.)
-DNDEBUG: Disable assertions
Technique 2: Profile-Guided Optimization (PGO)
Step 1: Build with profiling:
cmake -B build-pgo -DCMAKE_CXX_FLAGS="-fprofile-generate"
cmake --build build-pgo
Step 2: Run representative workload:
./build-pgo/your_app # Generate profiling data
Step 3: Rebuild with profile data:
cmake -B build-pgo-opt -DCMAKE_CXX_FLAGS="-fprofile-use"
cmake --build build-pgo-opt
Benefit: 5-15% performance improvement.
Technique 3: Huge Pages (Linux)
Enable huge pages for orderbook memory:
#include <sys/mman.h>
void enableHugePages() {
system("echo always > /sys/kernel/mm/transparent_hugepage/enabled");
}
Benefit: Reduces TLB misses for large multi-symbol deployments.
Technique 4: NUMA Awareness
Pin memory to local NUMA node:
#include <numa.h>
void setupNUMA() {
int node = numa_node_of_cpu(sched_getcpu());
numa_set_preferred(node);
}
Benefit: Reduces memory access latency.
Benchmarking
Running Benchmarks
Basic benchmarking:
# Build with benchmarks enabled
cmake -B build -DCMAKE_BUILD_TYPE=Release -DSLICK_ORDERBOOK_BUILD_BENCHMARKS=ON
cmake --build build -j
# Run L2 benchmarks
./build/benchmarks/bench_orderbook_l2
# Run specific benchmark with filter
./build/benchmarks/bench_orderbook_l2 --benchmark_filter="AddNewLevel/100"
# Control measurement duration
./build/benchmarks/bench_orderbook_l2 --benchmark_min_time=10
# Output to JSON for analysis
./build/benchmarks/bench_orderbook_l2 --benchmark_out=results.json --benchmark_out_format=json
Interpreting Results
Benchmark output:
BM_L2_AddNewLevel/10 21.2 ns 21.2 ns 33057584
BM_L2_AddNewLevel/100 28.4 ns 28.5 ns 24549123
Key metrics:
- Time: Per-operation latency
- Iterations: Number of samples (higher = more reliable)
- Items/sec: Throughput (calculated automatically)
Custom Benchmarks
Template for custom benchmarks:
#include <benchmark/benchmark.h>
static void BM_MyWorkload(benchmark::State& state) {
for (int i = 0; i < 100; ++i) {
}
for (auto _ : state) {
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_MyWorkload);
BENCHMARK_MAIN();
Level 2 OrderBook - aggregated price levels.
Profiling
Visual Studio Performance Profiler (Windows)
Quick profiling:
- Open Visual Studio 2022
- Debug → Performance Profiler (Alt+F2)
- Select "CPU Usage (sampling)"
- Target:
bench_orderbook_l2.exe
- Run and analyze Hot Path view
See benchmarks/PROFILING_GUIDE.md for detailed instructions.
Linux perf
Profile hot functions:
# Record profiling data
perf record -g ./bench_orderbook_l2
# View report
perf report
# Look for:
# - updateLevel
# - std::lower_bound (binary search)
# - observer callbacks
Intel VTune
Cache miss analysis:
# Profile cache behavior
vtune -collect memory-access -- ./bench_orderbook_l2
# View results
vtune-gui
Expected findings:
- L1 cache miss rate: <1%
- L2 cache miss rate: <5%
- Hot functions:
std::lower_bound, updateLevel
Performance Targets
Target Latencies (p99)
| Operation | Target | Status |
| L2 Add/Modify/Delete | <100ns | ✅ Achieved (21-33ns) |
| L3 Add/Modify/Delete | <200ns | ✅ Achieved (59-490ns) |
| Best Bid/Ask Query | <10ns | ✅ Achieved (0.25ns) |
| Observer Notification | <50ns/observer | ✅ Achieved (2-3ns) |
Target Throughput
| Scenario | Target | Status |
| Single Symbol L2 | >10M updates/sec | ✅ Achieved (30-48M) |
| Single Symbol L3 | >5M updates/sec | ✅ Achieved (12-17M) |
| 100 Symbols | >5M items/sec | ✅ Achieved (5.5M) |
Memory Targets
| Configuration | Target | Status |
| L2 per symbol | <1KB | ✅ Achieved (320B base) |
| L3 per 1000 orders | <10KB | ✅ Achieved (~88KB includes pool) |
Conclusion
Slick OrderBook is designed for ultra-low latency with all performance targets exceeded by 2-40x. Follow the best practices in this guide to achieve optimal performance in your application.
Key Takeaways:
- Choose the right orderbook type (L2 vs L3)
- Pre-allocate when possible
- Minimize observer count
- Use zero-copy APIs
- Always benchmark in Release mode
- Profile before optimizing
- Thread affinity helps in critical paths
For more information: