Slick OrderBook 1.1.0
High-Performance C++23 OrderBook Library
Loading...
Searching...
No Matches
Performance Guide

This guide provides best practices and recommendations for achieving optimal performance with Slick OrderBook.

Table of Contents

  1. Performance Characteristics
  2. Best Practices
  3. Common Pitfalls
  4. Optimization Techniques
  5. Benchmarking
  6. 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);
// Pre-allocate space for expected order count
book.reserve(1000); // Avoids pool growth during trading hours

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);
// Notifies observers for each level!
}

Do:

// Notify observers once for the entire snapshot
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 {
// Only react to top 10 levels
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); // COPY!

Do:

const auto& levels = book.getLevels(Side::Buy); // Zero-copy
for (const auto& level : levels) {
// Process level
}

Benefit: Eliminates unnecessary copies.

7. Cache TopOfBook Queries

If querying multiple times per update:

// Cache the result
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:

// For 4 decimal places: multiply by 10000
Price price = 100.2550 * 10000; // 1002550
book.updateLevel(Side::Buy, price, quantity, ts, seq);
// To display:
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);
}
// In your market data handler thread:
pinToCPU(2); // Pin to CPU 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; // EXPENSIVE COPY!

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);
// Each update notifies all observers!
}

Why it's bad: O(n × m) where n = updates, m = observers.

Solution: Batch updates or use snapshot mode.

❌ Pitfall 3: Unnecessary Conversions

// Getting L2 from L3 repeatedly
for (...) {
auto levels = l3_book.getLevelsL2(Side::Buy); // O(n) aggregation
}

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);
// Use levels multiple times

❌ Pitfall 4: Not Using OrderBookManager

std::map<SymbolId, OrderBookL2> books; // Manual management

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

// With NDEBUG not defined:
book.updateLevel(...); // Includes assertion checks

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>
// In your initialization code:
void enableHugePages() {
// Configure system for huge pages
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) {
// Setup phase (not timed)
for (int i = 0; i < 100; ++i) {
book.updateLevel(slick::orderbook::Side::Buy, 10000 - i * 10, 1000, 0, 0);
}
// Benchmark loop (timed)
for (auto _ : state) {
// Your operation here
book.updateLevel(slick::orderbook::Side::Buy, 10000, 1100, 0, 0);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_MyWorkload);
BENCHMARK_MAIN();
Level 2 OrderBook - aggregated price levels.

Profiling

Visual Studio Performance Profiler (Windows)

Quick profiling:

  1. Open Visual Studio 2022
  2. Debug → Performance Profiler (Alt+F2)
  3. Select "CPU Usage (sampling)"
  4. Target: bench_orderbook_l2.exe
  5. 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:

  1. Choose the right orderbook type (L2 vs L3)
  2. Pre-allocate when possible
  3. Minimize observer count
  4. Use zero-copy APIs
  5. Always benchmark in Release mode
  6. Profile before optimizing
  7. Thread affinity helps in critical paths

For more information: