Skip to content

Commit 3e2a3a1

Browse files
committed
Feat: .cpp comments, README.md, documentation & compare_runs.py
1 parent df4f711 commit 3e2a3a1

20 files changed

Lines changed: 592 additions & 66 deletions

README.md

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,74 @@
11
# Evaluating the performance of lock-free data structures under realistic exchange workloads
2-
Louis Tanak, University of Warwick Third Year Computer Science Project
2+
Louis Tanak, University of Warwick Third Year Computer Science Project 2026.
33

44
## Abstract
5-
Lock-free data structures allow multiple threads to operate safely on shared data without using locks. These have many real-word use cases in high-performance computing, low-latency systems and financial exchanges. Research has been conducted on their theoretical properties, however there is a gap in understanding how lock-free data structures perform under realistic workloads, such as trading exchange-like patterns.
5+
Lock-free data structures are concurrent data structures that allow multiple threads to operate without the use of locks for synchronisation. They utilise atomic hardware instructions such as compare-and-swap (\acrshort{CAS}) or fetch-and-add (\acrshort{FAA}) to guarantee that at least one thread makes progress in a finite number of steps. Due to their ability to reduce contention and improve scalability, lock-free data structures have various applications primarily in high-performance systems such as web servers, physics engines and high-frequency trading systems.
66

7-
The work aims to implement various lock-free data structures and evaluate their performance against each other and traditional lock-based solutions. The data structures will be benchmarked against simulated exchange traffic to measure throughput, latency, scalability and completion ordering. Results will provide insights into the benefits, drawbacks and the suitable conditions for each data structure.
7+
Modern research covers different implementations of various lock-free data structures, to improve their latency and scalability with increasing thread counts. However, there currently lacks standardised frameworks to empirically evaluate lock-free data structures. Further to this, studies primarily focus on throughput and latency under synthetic workloads, whilst little work investigates order-preservation properties and their effects on downstream systems. These are critical properties in applications such as financial exchanges, where fairness and correctness depend on ordering guarantees.
8+
9+
This project evaluates the performance of lock-free data structures under realistic exchange workloads. We present a low-latency benchmarking framework, which provides concurrent measurement of latencies, order-preservation and implements a price-time priority matching engine to assess the downstream effects of ordering anomalies. Using the framework, we present a performance analysis of various lock-free data structure implementations and their characteristics. We conclude that the framework can effectively evaluate performance and ordering properties, providing a reusable suite for evaluating lock-free data structures.
10+
11+
## Supervisor Running Instructions
12+
This section is a quickstart intended for the project supervisor. It walks through running any one of the six lock-free data-structure implementations out of the box and viewing the generated report.
13+
14+
### 1. Compile the project
15+
From the repository root, run:
16+
```bash
17+
./compile.sh
18+
```
19+
20+
### 2. Select the data structure to evaluate
21+
Open `src/main.cpp`. Near the top of `main()` you will find six clearly-labelled blocks under the **DATA STRUCTURE SELECTION** banner:
22+
23+
1. `RegularQueue` - lock-based baseline
24+
2. `MCConcurrentQueue` - moodycamel ConcurrentQueue (MPMC)
25+
3. `MCLockFreeQueue` - moodycamel ReaderWriterQueue (SPSC only)
26+
4. `WiltMPMCBlockRing` - Wilt MPMC blocking ring buffer
27+
5. `WiltMPMCNonBlockRing` - Wilt MPMC non-blocking ring buffer
28+
6. `RigtorpMPMCQueue` - Rigtorp MPMC queue *(active by default)*
29+
30+
Uncomment **exactly one** block and comment out the others, then re-run `./compile.sh`.
31+
32+
By default, `RigtorpMPMCQueue` is uncommented.
33+
34+
### 3. Run the benchmark
35+
The recommended testing scenario runs both the stress and order scenarios under the same run ID:
36+
```bash
37+
./run.sh --all 4 1000
38+
```
39+
The arguments are:
40+
- `4` - number of producer/consumer threads (use `1` for SPSC-only structures such as the moodycamel ReaderWriterQueue)
41+
- `1000` - total orders driven through the system
42+
43+
The script prints a run ID of the form `XXXXXXXXXX` at the start and end of the run. **Copy this run ID**, as it is needed for the report step.
44+
45+
### 4. Generate the HTML report
46+
Switch into the Python benchmarking directory and generate the report for the run ID from step 3:
47+
```bash
48+
cd src/benchmarking/python
49+
pixi run report --run-id=XXXXXXXXXX
50+
```
51+
(Replace `XXXXXXXXXX` with the actual ID printed by `run.sh`.)
52+
53+
The first invocation of `pixi run` will download the Python environment, with subsequent calls being faster once the environment is resolved.
54+
55+
### 5. View the report
56+
The generated HTML report is written to:
57+
```
58+
results/reports/report_<run_id>.html
59+
```
60+
Open this file in any browser. It contains latency plots, ordering summaries, exchange-matching results and hardware counter data for the selected data structure.
61+
62+
### Full supervisor walkthrough (copy-paste)
63+
```bash
64+
./compile.sh
65+
# edit src/main.cpp, uncomment the desired data structure, re-run compile.sh
66+
./run.sh --all 4 1000
67+
# note the run ID printed by run.sh
68+
cd src/benchmarking/python
69+
pixi run report --run-id=<RUN_ID>
70+
# open results/reports/report_<RUN_ID>.html in a browser
71+
```
872

973
## Adding your own Lock-free Data Structure
1074
To begin benchmarking your lock-free data structure, perform the following:
@@ -157,4 +221,10 @@ If you want direct plots instead of the report:
157221
158222
If you would prefer to look at the code, please see: `pixi.toml` and `main.py` (both are unique names in this codebase!)
159223
160-
Note: The plotting code is actively evolving. The report mode is the most stable path for benchmarking.
224+
### Cross-run latency comparison
225+
To overlay latency graphs from multiple runs onto a single image (useful for comparing different data structures), see `src/benchmarking/python/image_editing/compare_runs.py` or run:
226+
- `pixi run compare --run-ids 1234567 7654321 1122334`
227+
228+
Output images are saved into `src/benchmarking/python/image_editing/`.
229+
230+
Note: The plotting code is actively evolving. The report mode is the most stable path for benchmarking.

documentation/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Framework Documentation
2+
3+
This directory contains reference documentation for the lock-free data-structure benchmarking framework. It is split into two parts:
4+
5+
- [`cpp.md`](cpp.md) - the C++ benchmarking core (data structures, scenarios, benchmarking harness, hardware logging)
6+
- [`python.md`](python.md) - the Python analysis pipeline (plotting, report generation, cross-run comparison, image editing)
7+
8+
For a quickstart aimed at running the framework end-to-end, see the **Supervisor Instructions** section in the top-level [`README.md`](../README.md).
9+
10+
## High-level architecture
11+
12+
The framework is split across three phases: a C++ execution phase that drives producer/consumer threads through the selected data structure, a post-processing phase that emits one CSV per metric category, and a Python analysis phase that turns those CSVs into an HTML report.
13+
14+
![System architecture](images/system_architecture.png)
15+
16+
The C++ side produces tagged CSV files under `results/`. The Python side consumes those CSVs, groups them by `run_id`, and produces either per-plot images or a single aggregated HTML report.

documentation/cpp.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# C++ Framework
2+
3+
The C++ side of the project does three things: it owns the data structures under test, it drives them with realistic-ish exchange traffic, and it dumps measurements to CSV for the Python pipeline to pick up. This document is a tour of how that's wired together.
4+
5+
## Layout
6+
7+
Headers live under `include/`, implementations under `src/`, and the two trees mirror each other:
8+
9+
```
10+
include/
11+
benchmarking/ BenchmarkWrapper template
12+
data_structures/
13+
queues/ IQueue interface + queue implementations
14+
ring_buffers/ IRing interface + ring buffer implementations
15+
exchange/ Matching engine, order book, price levels
16+
hardware_logging/ PAPI counter wrappers
17+
order_simulation/ Synthetic order generators
18+
scenarios/ Stress / order scenario drivers and CLI parsing
19+
utils/ Timing, threading, file and struct helpers
20+
21+
src/
22+
main.cpp Entry point and data structure selection
23+
benchmarking/cpp/benchmark.cpp Explicit BenchmarkWrapper instantiations
24+
data_structures/ Queue / ring implementations
25+
exchange/ Matching engine
26+
hardware_logging/ PAPI wrapper
27+
order_simulation/ Order generators
28+
scenarios/ CLI parsing (test_inputs)
29+
utils/ Timing / threading / file helpers
30+
```
31+
32+
## main.cpp
33+
34+
`main.cpp` is intentionally small. It parses arguments into a `TestParams`, picks a ring-buffer capacity based on the workload, and constructs a `BenchmarkWrapper` around exactly one of the six candidate data structures. The six candidates are arranged as mutually-exclusive blocks under a "DATA STRUCTURE SELECTION" banner — to switch which one is benchmarked, comment the active block out and uncomment the one you want, then rebuild. Once the wrapper exists, `main` just dispatches into either the stress or the order scenario and exits.
35+
36+
## BenchmarkWrapper
37+
38+
`BenchmarkWrapper<TDS, TOrder>` (in `include/benchmarking/benchmark.hpp`) is the heart of the framework. It is templated on both the data structure type and the order type, so any class implementing `IQueue` or `IRing` slots in without modification.
39+
40+
![BenchmarkWrapper class diagram](images/benchmark_wrapper_class.png)
41+
42+
The wrapper handles everything around the hot path: it tracks producer and consumer thread IDs so per-thread metrics make sense, takes nanosecond timestamps either side of every enqueue and dequeue, attaches sequence numbers so the Python side can reason about FIFO ordering, runs orders through the matching engine when the order scenario asks it to, and wraps each measured region in start/stop calls to `HardwareLogger`. When a scenario finishes the wrapper writes one CSV per metric category into `results/`, tagged with the run ID and timestamp that `run.sh` passed in.
43+
44+
Because every supported data structure needs its own template instantiation, those instantiations are collected at the bottom of `src/benchmarking/cpp/benchmark.cpp`. Adding a new data structure means adding one more `template class BenchmarkWrapper<...>;` line there.
45+
46+
## Data structures
47+
48+
There are two interfaces, `IQueue<TOrder, Derived>` for queue-style structures and `IRing<TOrder, Derived>` for bounded ring buffers. Both are CRTP, so calls from the wrapper into the data structure are statically dispatched and the harness contributes no virtual-call overhead to the measurements. The contract for both interfaces is the same five operations:
49+
50+
```cpp
51+
auto enqueueOrder(TOrder &order) -> bool;
52+
auto dequeueOrder(TOrder &order) -> bool;
53+
auto getSize() -> uint64_t;
54+
auto isEmpty() -> bool;
55+
auto getFront(TOrder &order) -> bool;
56+
```
57+
58+
The recipe for adding a new data structure lives in the top-level README under "Adding your own Lock-free Data Structure".
59+
60+
## Scenarios
61+
62+
There are two scenarios, `stress` and `order`, defined under `include/scenarios/`. Stress just hammers the data structure with as many enqueue/dequeue operations as the configured thread count and order count permit; it produces latency and hardware CSVs. Order is the more interesting one — it drives a mixed workload through the matching engine, checks per-item ordering, and emits ordering, exchange and hardware CSVs.
63+
64+
CLI parsing lives in `scenarios/test_inputs.hpp`. It populates a `TestParams` from `argv`, including the positional `mode / threads / orders` arguments and the optional `--seed` and `--run-id` flags that `run.sh` always supplies.
65+
66+
## Order simulation
67+
68+
The synthetic order flow lives under `include/order_simulation/`. Each generator implements `IOrderGenerator`, and `collection_order_generator` multiplexes across several sub-generators (random, momentum, mean-reverting, market maker) using a shared `MarketState` so the resulting traffic looks roughly like real exchange flow rather than uniform noise. `BenchmarkOrder` is the POD passed through the data structures; everything in the C++ side is templated over it.
69+
70+
## Exchange
71+
72+
`include/exchange/` contains a small limit-order-book matching engine. It is deliberately not a full exchange — it just consumes orders from the data structure, updates the price book and per-level state, and emits a trades cycle that the wrapper writes out to the exchange CSV. The Python side then compares those trades against the expected trades for the input sequence.
73+
74+
## Hardware logging
75+
76+
`HardwareLogger` is a thin wrapper over PAPI for collecting cycles, instructions retired, L1/L2 cache misses and branch mispredictions. `thread_counter` keeps the counters per-thread, which is what lets the hardware CSV distinguish producer behaviour from consumer behaviour. `hardware_metrics` defines the schema that ends up as the CSV header.
77+
78+
## Utilities
79+
80+
The `utils/` directory is where the smaller helpers live. `timing.hpp` exposes nanosecond clocks and RDTSC; `threads.hpp` handles thread pinning; `files.hpp` knows how to build the `<category>_<run_id>_<timestamp>.csv` filenames the Python side expects; `structs.hpp` holds shared PODs; `counter.hpp` has a couple of small atomic counters.
81+
82+
## Build
83+
84+
Building is one command via CMake + Ninja:
85+
86+
```bash
87+
./compile.sh
88+
```
89+
90+
The result is a single executable at `build/lock_free_data_structures` which `run.sh` invokes with the appropriate flags.
91+
92+
## End-to-end flow
93+
94+
From a single `./run.sh` invocation, here is what happens:
95+
96+
![Process flow](images/process_flow.png)
97+
98+
`run.sh` generates a random run ID and launches the executable with the parsed arguments and `--run-id <RUN_ID>`. `main.cpp` constructs the chosen data structure plus a wrapper around it, then hands control to the stress or order scenario. The scenario drives the wrapper; the wrapper accumulates latency, ordering and hardware counter samples in memory. When the scenario finishes the wrapper writes one CSV per category into the right subdirectory of `results/`. From there the Python pipeline (covered in [`python.md`](python.md)) picks the CSVs up by run ID and turns them into plots or an aggregated HTML report.
91.3 KB
Loading
568 KB
Loading
682 KB
Loading

0 commit comments

Comments
 (0)