Corex is a production-quality, low-latency matching engine written in modern C++17. It is designed for correctness, performance, and maintainability, making it suitable for high-frequency trading simulations or crypto-exchange backends.
The project strictly focuses on zero-allocation fast paths, cache-friendly data structures, and predictable performance.
Corex relies on several advanced C++ paradigms and system design principles to achieve sub-microsecond latency and uncompromising correctness:
-
Zero-Allocation Hot Path via Intrusive Containers Instead of utilizing
std::listor vectors for the orderbook's price levels (which would cause continuous heap allocations and cache misses), Corex embedsboost::intrusive::list_base_hookdirectly inside theOrderstructures. This ensures that inserting, moving, or canceling an order is reduced to mere pointer manipulation without invoking the system memory allocator, avoiding OS-level latency spikes during peak market volatility. -
O(1) Symbol Lookup via
robin_hoodHashing To route millions of orders per second to the correct orderbook, Corex replaces the standardstd::unordered_mapwithrobin_hood::unordered_map. Standard library unordered maps use separate chaining (linked lists for buckets), causing severe cache thrashing due to pointer-chasing. Therobin_hoodflat hash map utilizes open addressing with linear probing and backward shift deletion, ensuring that symbol routing is executed almost entirely out of the CPU's L1/L2 cache for ultra-fast, predictable O(1) lookups. -
Dual-Mode Execution Architecture
Market(Deterministic Single-Threaded): Designed for extreme low-latency environments where locking overhead is unacceptable. It operates entirely lock-free from the caller's thread, processing sequences in deterministic order for reproducible backtesting and high-frequency trading.ConcurrentMarket(Asynchronous Thread-Pool): Utilizes a dedicated thread pool and condition-variable backed lock queues for offloading orderbook management. Designed for scenarios where multiple worker threads process disconnected symbols concurrently, allowing frontend producers to submit orders asynchronously without blocking.
-
Comprehensive Order Types with O(1) Execution
- Limit & Market Orders: Matches aggressively against the
MapOrderBookliquidity. - Fill-Or-Kill (FOK): Atomic execution ensuring either full quantity is instantly filled or the order is entirely canceled, crucial for zero-slippage strategies.
- Stop & Trailing Stop: Maintained in separate internal triggers and activated efficiently into the main book. Trailing stops dynamically adjust to market prices using efficient C++17
std::map::extract()node manipulation, avoiding full container rebuilds.
- Limit & Market Orders: Matches aggressively against the
-
Event-Driven Zero-Copy Snapshots To integrate with external gateways or risk-management systems, Corex emits events (
OrderAdded,ExecutedOrder,OrderDeleted). Instead of blindly copying the 100+ byte internalOrderobjects (which contain intrusive list hooks), it constructs lightweight, stripped-downOrderSnapshotobjects by value. This decouples internal book state from external event handlers and significantly reduces memory bandwidth requirements. -
Robust Boundary Validation & Modern C++17 The engine leverages move semantics,
constexpr, and strictly-defined exceptions (std::invalid_argument) at the API boundary, guaranteeing that internal algorithms only execute on validated, sanitized data. This eliminates undefined behavior (like unsigned underflows or infinite loops) when processing malformed market inputs.
Corex uses CMake for its build system. Dependencies like Google Test, Google Benchmark, and spdlog are fetched automatically.
# Clone the repository
git clone https://github.com/yourusername/corex.git
cd corex
# Create a build directory
mkdir build && cd build
# Configure and compile with optimizations enabled
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)After building, you can run the examples provided in the bin/ directory:
# Run the single-threaded market example
./bin/corex_market_example
# Run the concurrent market example
./bin/corex_concurrent_market_exampleThe test suite validates the correctness of the matching algorithms, orderbook updates, and error handling.
# Run all unit tests
./bin/corex_testsCorex includes comprehensive microbenchmarks for both the single-threaded (Market) and multi-threaded (ConcurrentMarket) engines. Benchmarks measure the time taken to process millions of orders across varying numbers of symbols (orderbooks).
Test Environment Specs:
- CPU: 12th Gen Intel(R) Core(TM) i7-12700H (14 Cores / 20 Threads, up to 4.7 GHz)
- Caches: L1: 1.25 MiB, L2: 11.5 MiB, L3: 24 MiB
- OS: Linux (x86_64)
To reproduce these benchmarks on your machine:
./bin/corex_benchmark
./bin/concurrent_corex_benchmark| Benchmark | Time (ms) | CPU (ms) | Iterations |
|---|---|---|---|
BM_Market/symbols:1/orders:1000000 |
199 | 199 | 3 |
BM_Market/symbols:1/orders:2000000 |
419 | 419 | 2 |
BM_Market/symbols:1/orders:3000000 |
619 | 619 | 1 |
BM_Market/symbols:1/orders:4000000 |
909 | 909 | 1 |
BM_Market/symbols:100/orders:1000000 |
210 | 210 | 3 |
BM_Market/symbols:100/orders:2000000 |
492 | 492 | 2 |
BM_Market/symbols:100/orders:3000000 |
781 | 780 | 1 |
BM_Market/symbols:100/orders:4000000 |
1126 | 1126 | 1 |
BM_Market/symbols:1000/orders:1000000 |
315 | 315 | 2 |
BM_Market/symbols:1000/orders:2000000 |
981 | 981 | 1 |
BM_Market/symbols:1000/orders:3000000 |
1146 | 1145 | 1 |
BM_Market/symbols:1000/orders:4000000 |
1638 | 1632 | 1 |
BM_Market/symbols:2000/orders:1000000 |
446 | 444 | 2 |
BM_Market/symbols:2000/orders:2000000 |
849 | 849 | 1 |
BM_Market/symbols:2000/orders:3000000 |
2170 | 2169 | 1 |
BM_Market/symbols:2000/orders:4000000 |
2421 | 2420 | 1 |
| Benchmark | Time (ms) | CPU (ms) | Iterations |
|---|---|---|---|
BM_ConcurrentMarket/symbols:1/orders:1000000 |
492 | 329 | 2 |
BM_ConcurrentMarket/symbols:1/orders:2000000 |
1102 | 750 | 1 |
BM_ConcurrentMarket/symbols:1/orders:3000000 |
1639 | 1097 | 1 |
BM_ConcurrentMarket/symbols:1/orders:4000000 |
2225 | 1452 | 1 |
BM_ConcurrentMarket/symbols:100/orders:1000000 |
562 | 527 | 2 |
BM_ConcurrentMarket/symbols:100/orders:2000000 |
1255 | 1164 | 1 |
BM_ConcurrentMarket/symbols:100/orders:3000000 |
1666 | 1552 | 1 |
BM_ConcurrentMarket/symbols:100/orders:4000000 |
2338 | 2197 | 1 |
BM_ConcurrentMarket/symbols:1000/orders:1000000 |
503 | 476 | 2 |
BM_ConcurrentMarket/symbols:1000/orders:2000000 |
976 | 803 | 1 |
BM_ConcurrentMarket/symbols:1000/orders:3000000 |
1305 | 1185 | 1 |
BM_ConcurrentMarket/symbols:1000/orders:4000000 |
1777 | 1619 | 1 |
BM_ConcurrentMarket/symbols:2000/orders:1000000 |
471 | 440 | 2 |
BM_ConcurrentMarket/symbols:2000/orders:2000000 |
946 | 883 | 1 |
BM_ConcurrentMarket/symbols:2000/orders:3000000 |
1670 | 1297 | 1 |
BM_ConcurrentMarket/symbols:2000/orders:4000000 |
1940 | 1525 | 1 |
You may notice from the benchmark results that the ConcurrentMarket is generally slower in real execution time than the single-threaded Market, particularly for workloads concentrated on fewer symbols (e.g., processing 1M orders for a single symbol takes ~199ms sequentially vs. ~492ms concurrently).
This is a classic phenomenon in low-latency systems.
Why is concurrent slower here?
- Synchronization Overhead: Every order submitted to the concurrent market is packaged into a task, pushed onto a queue protected by a
std::mutex, and signals astd::condition_variable. A worker thread must then wake up, lock the mutex, pop the task, and execute it. Because internal matching operations (like finding a node instd::map) take mere nanoseconds, the OS-level locking and thread wake-up times vastly overshadow the actual algorithmic work. - Single-Symbol Contention: When all orders target the same symbol (
symbols:1), parallelization offers zero benefit because the matching logic for a single orderbook must be strictly serialized. Threads end up contending for the same cache lines or waiting on each other. - Cache Line Bouncing: Passing order data from a producer thread to a consumer worker thread forces CPU caches to be invalidated and synchronized across cores, introducing significant memory latency compared to a single-threaded loop operating out of L1 cache.
To enable the concurrent engine to outperform the single-threaded engine under high load, the architecture will be evolved to address these bottlenecks:
- Sharding by Symbol: Instead of a generic global task queue, orderbooks (symbols) can be pinned to specific worker threads (sharded architecture). This completely eliminates mutexes during order execution—producer threads route orders to the correct thread-local queue, and workers operate lock-free on their assigned symbols.
- Lock-Free Queues: Replacing
std::mutexqueues with Single-Producer Single-Consumer (SPSC) or Multi-Producer Single-Consumer (MPSC) lock-free ring buffers (similar to the LMAX Disruptor pattern) will remove system calls and context-switching overhead from the hot path. - Task Batching: Grouping incoming orders into batches before dispatching them to worker threads can significantly amortize synchronization costs.