ACMTOrderBook 是一个面向 A 股 L2 逐笔行情的高性能订单簿重建/撮合引擎,基于 AXOrderBook 的 C++ 多线程重写。它接收深交所真实的逐笔委托 / 逐笔成交 / 十档快照三类消息,重建出连续、可逐位校验的订单簿状态。
提供四种实现:
- Python 参照实现(
py/)— 行为基准,用于正确性对照; - C++ v1(单线程)— 基础重写;
- C++ v2(多线程 · 当前引擎)—
SPSC 无锁队列 + 读写分离双线程,结合零拷贝 mmap、HybridLevelBook、平铺哈希、大页缓存等优化。
核心亮点
- 🎯 性能(深交所 000001 2022-04-22,233,875 条,T1 系统端到端实测):Python
1,575 msg/s→ C++ v2.6≈1.29M msg/s(约 820×) - 🧵 架构:双线程读写分离、无锁队列、零拷贝 mmap、批量/预取、缓存行对齐、CPU 亲和性、QPC 单调计时
- 📊 能力:逐笔订单簿重建、十档快照、委托队列展示、GUI
dashboard.py仪表盘、秒级 K 线合成 - ✅ 正确性:各版本重建的订单簿状态逐位一致,且与交易所官方十档快照完全吻合(
NumTrades=81,049、LastPx=16060000(×10⁶=16.06元)、HighPx=16190000、LowPx=15400000) - 🗂 可复现:
versions/收录 v1 → v2.6 各历史版本源码(按 commit 检出),并在同一环境下统一实测性能
演进脉络:从 Python 1,575 msg/s 到 C++ v2.6 ≈1.29M msg/s,吞吐跃迁主要来自 mmap 文件预加载、直接字段解析(跳过 std::map/临时 string),以及引擎侧的 HybridLevelBook / genSnap / 前向 strstr 优化;详见性能测试。
- 🚀 极致性能:全版本演进在真实数据上实测,C++ v2.6 系统端到端 ≈
1.29M msg/s(详见性能测试) - 🧵 双线程读写分离:SPSC 无锁队列,生产者/消费者并行处理
- 📊 完整能力:逐笔订单簿重建、十档快照、委托队列展示
- 🖥️ GUI 仪表盘:
dashboard.py对比 Python 与 C++ 版本 - 🔀 跨实现:Python 参照实现 + C++ v1 / v2(Windows),同一内核跨平台(MSVC / GCC)
- 🗂 版本管理:
versions/收录各历史版本源码(按 commit 检出,见版本管理)
测试基准与数据:深交所 000001(平安银行)2022-04-22 全日 L2,共 233,875 条消息 (逐笔委托 122,359 + 逐笔成交 106,434 + 十档快照 5,082;深交所 L2 快照深度 10 档,千档委托队列不在样例中)。单次重放实测。
测试环境:Windows 沙箱,MinGW g++ 8.1.0(-O3 -march=native -funroll-loops -flto),QPC 分辨率 ≈100 ns。实测统一使用 -DUSE_FLAT_HASHMAP=0(默认 std::unordered_map,因所需的 ankerl 子模块未随仓库提供)、-DUSE_MMAP=1。
度量口径:
- 吞吐 = T1 系统端到端吞吐:总消息数 ÷ 整段墙钟时间(含文件读/mmap、解析、生产者、SPSC 队列、双线程并行、引擎),反映全部架构 + 数据路径 + 引擎优化——双线程、零拷贝/mmap、批量/预取、大页缓存等都体现在 T1。
- 延迟 = L1 单事件处理延迟:每条真实消息
onMsg()的处理耗时,逐条全量计时,报告p50 / p99 / p99.9 / pmax。 - 计时约定:单调时钟(Windows
QueryPerformanceCounter,Linuxsteady_clock,Pythontime.perf_counter_ns());百分位索引floor(p×(N-1))。
| 版本 | T1 吞吐量 | p50 | p99 | pmax | 核心优化 |
|---|---|---|---|---|---|
Python (py/) |
1,575 msg/s | 447.9 µs | 2,296.4 µs | 845,481.6 µs | 基线参照 |
| C++ v1 | 102,573 msg/s | 0.6 µs | 3.0 µs | 3,246.4 µs | 单线程 C++ 重写 |
| C++ v2 | 106,066 msg/s | 0.6 µs | 2.1 µs | 1,597.6 µs | SPSC 无锁双线程 |
| C++ v2.1 | 102,918 msg/s | 0.3 µs | 1.0 µs | 1,743.7 µs | HybridLevelBook + 预取 |
| C++ v2.2 | 267,175 msg/s | 0.2 µs | 0.9 µs | 1,354.0 µs | mmap 文件预加载 + 平铺哈希 |
| C++ v2.3 | 853,527 msg/s | 0.2 µs | 0.8 µs | 1,442.2 µs | 直接字段解析 |
| C++ v2.4 | 922,082 msg/s | 0.2 µs | 0.7 µs | 1,071.5 µs | 代码质量优化 |
| C++ v2.5 | 1,226,486 msg/s | 0.1 µs | 0.5 µs | 1,068.2 µs | genSnap 延迟重建 |
| C++ v2.6 | 1,292,270 msg/s | 0.1 µs | 0.6 µs | 855.3 µs | 前向 strstr + 延迟采样 + 条件拷贝 |
✅ 各版本重建的订单簿状态一致,且与交易所官方快照逐位吻合(内部价格统一 ×10⁶):
NumTrades=81,049 LastPx=16060000 HighPx=16190000 LowPx=15400000 OpenPx=15640000 TVol=9,212,740,800 TVal=14,684,529,579,500。 ✅ 延迟按 L1 单事件onMsg()、逐条全量计时,已统一切换为 QPC 单调时钟(100 ns 分辨率);p99.9亦可见于运行输出。
T1 是"系统端到端"吞吐:总消息数 ÷ 整段墙钟,包含文件读/mmap、解析、生产者、队列、双线程、引擎。在 v1 / v2 / v2.1 阶段,瓶颈在生产者的文件读取 + 解析(ifstream 逐行读 + parseKeyValueLine 每行 std::map + 临时 string):
- 实测引擎
onMsg单条仅 ~0.3–0.6 µs,233,875 条合计 ~0.14 s,而墙钟 ~2.3 s → onMsg 只占 ~6%,其余 ~94% 都是 I/O + 解析。 - v2 的 SPSC 双线程把"消费者(引擎)"与"生产者(I/O+解析)"解耦,但生产者仍单线程且是瓶颈,消费者大多在等 → T1 几乎不变(102.6K → 106.1K),仅引擎延迟略降。
- 真正吃掉该瓶颈的是 v2.2
mmap(消除逐行读 → 267K)与 v2.3 直接字段解析(跳过std::map/string → 853K)。只有在此之后,引擎侧优化(v2.1 HybridLevelBook、v2.5 genSnap、v2.6 strstr)才真正体现到系统吞吐。
💡 双线程是必要架构,但它本身不消除 I/O/解析瓶颈;吞吐的跃迁来自随后的 mmap / 直接解析。
当前工作区版 cpp v2 为 v2.6 的最终形态(含 QPC L1 计时),实测 T1 ≈1,352,950 msg/s,p50=0.1µs p99=0.6µs p99.9=1.1µs pmax≈1037µs。
- 红色曲线:本机实测 T1 系统端到端(000001 2022-04-22,233,875 条,纵轴对数)
- 曲线脚本:
plot_throughput.py
py/— Python 参照实现(行为基准)cpp v1/— 单线程 C++ 版本(基础重写,含完整订单簿功能)cpp v2/— 多线程优化版本,即当前引擎(含 SPSC 队列、mmap、HybridLevelBook、L1 延迟统计等)versions/— 历史版本源码归档(见下方版本管理)
versions/ 按 commit 检出各历史版本的源码,保留完整演进脉络,便于对比与复测。
| 版本目录 | 检出 commit | 说明 |
|---|---|---|
versions/v1 |
15cd00d |
单线程 C++ 基线 |
versions/v2 |
3a03928 |
SPSC 双线程 |
versions/v2.1 |
40bcac2 |
HybridLevelBook + 预取 |
versions/v2.2 |
a4fbf49 |
mmap 文件预加载 |
versions/v2.3 |
ae984dd |
直接字段解析 |
versions/v2.4 |
77c40eb |
代码质量优化 |
versions/v2.5 |
0f2c9dd |
genSnap 延迟重建 |
versions/v2.6 |
15cd00d |
前向 strstr + 延迟采样 |
MinGW 兼容性说明:这些历史版本为 MSVC 环境开发,在 MinGW 下编译需少量兼容修补(不改动引擎逻辑):
tool/field_parser.h:为 GCC/MinGW 引入<cpuid.h>,并将__cpuid替换为便携的__get_cpuid(SSE4.2 检测)。core/huge_pages.h:原文件为平台相关、未随源码提交;以versions/v2.5、versions/v2.6下的core/huge_pages.h桩文件提供allocLargePages/freeLargePages回退(返回nullptr,MemoryPool自动退回alignedAlloc,即"大页不可用"场景)。tool/axsbe_base.h(v2.4):补充<cstring>/<cstdlib>,供模板解析使用strstr/strncmp/strtoll。
- Windows 10/11;MSVC 2022(Visual Studio 2022);CMake ≥ 3.15;Git
1. 克隆项目
git clone https://github.com/MistyBridge/ACMTOrderBook.git
cd ACMTOrderBook2. Python 版本(可选)
cd py && python main.py3. C++ v1
cd "cpp v1"
g++ -std=c++17 -O0 -I. -o orderbook.exe main.cpp behave/*.cpp4. C++ v2(推荐,CMake + MSVC)
cd "cpp v2"
cmake -B build -G "Visual Studio 17 2022" -A x64 -DUSE_MMAP=ON -DUSE_FLAT_HASHMAP=1
cmake --build build --config Release --parallel 8
# 输出: build/Release/orderbook_v2.exe5. 运行测试
./build/Release/orderbook_v2.exe [数据文件] [生产者核心] [消费者核心] [队列容量] [批次大小] [重放次数]
# 示例
./build/Release/orderbook_v2.exe "../data/AX_sbe_szse_000001/AX_sbe_szse_000001.log" 0 2 16384 64 1引擎也可用 GCC/MinGW 编译(性能测试环境即 MinGW g++ 8.1)。单元测试见下文「工程化」;编译器需支持 posix 线程以构建 GoogleTest。
- 使用 Release 模式编译
- 确保 CPU 支持 AVX2
- 生产者绑 Core 0、消费者绑 Core 2
- 启用 mmap(默认开启)与平铺哈希表(ankerl,默认开启)
cpp v2 内置 GoogleTest 单元测试(FETCHCONTENT 拉取,third_party/ 被 gitignore 故不 vendor)。覆盖 SPSCQueue、HybridLevelBook、LatencyStats、MemoryPool、field_parser、AXOB 冒烟。运行:
cd "cpp v2"
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DUSE_FLAT_HASHMAP=0 -DBUILD_TESTING=ON
cmake --build build --config Release --target unit_tests --parallel 8
ctest --test-dir build -C Release --output-on-failureMSVC 一键版(更省事):已内置
CMakePresets.json,直接cd "cpp v2" cmake --preset test # configure + build unit_tests (BUILD_TESTING=ON) cmake --build --preset test ctest --preset unit # 跑 GoogleTest注:GoogleTest 需要 posix 线程(
std::thread/condition_variable);本引擎用 Win32CreateThread所以能在线程模型受限的 MinGW 上编译,但 gtest 需要标准 C++ 线程。Linux/gcc、MSVC 均可直接编译 gtest。引擎源码已去除__int128(改为交易所原生int64精度),故 MSVC 也能编译整个引擎。
.github/workflows/ci.yml:push/PR 到 main 触发,在 ubuntu-latest(gcc)上 configure(USE_FLAT_HASHMAP=0, BUILD_TESTING=ON)→ build → ctest。只设 Linux job(引擎是 GCC/Clang 目标,且 gtest 需 posix 线程)。
两遍构建已接入 CMake(-DPGO_MODE=GEN|USE),并有可复现脚本:
./scripts/run_pgo.sh <data_file> # GEN -> 运行出 .gcda -> USE -> 打印基线 vs PGO先用合成数据即可:python tools/gen_market_data.py --out data_synth.log --msgs 200000。
诚实实测:在合成 200k 条(MinGW g++ 8.1,
-O3 -flto)下,PGO 对引擎纯处理(T2)与系统端到端(T1)均在噪声内(~±1%),无明显收益。原因:引擎已-O3 -flto+LIKELY/UNLIKELY分支提示;PGO 主要对分支/代码布局密集、未显式标注的代码收益大。 采纳决定:PGO 已采纳为可选构建模式——我们验证了它无副作用(基线版与 PGO_USE 版在相同数据上产出逐位一致的确定性功能结果;差异仅是双线程进度打印的交错顺序),且让系统更泛用(可对不同数据/机器生成 profile 以适配热点)。在合成负载上无实测增益,但真实数据/真实热分布下更可能获益。故保留scripts/run_pgo.sh作为标准两遍构建流程。
benchmark/bench_simd.cpp 对 L2 字段串扫描做三路 A/B(/arch:AVX2;含正确性等价校验,三实现 foundA/foundB 全部一致):
| 场景 | scalar strstr |
SSE4.2 _mm_cmpistri |
AVX2 _mm256 |
|---|---|---|---|
| 短 L2 行(~150B,生产负载) | 100,199 | 52,828(-1.9×) | 50,263(-2.0×) |
| 长文本(~2KB,理论 SIMD 强项) | 69,794 | 8,296(-8.4×) | 8,598(-8.1×) |
结论:无论短行还是长文本,手写 SSE4.2 / AVX2 都明显慢于 scalar
strstr。原因:CRT 的strstr内部已用 SIMD(甚至 AVX2)实现,手写_mm256/_mm_cmpistri反而多出load+movemask+ctz+memcmp的开销(与field_parser.h“SIMD 曾回滚”注释一致)。按“只有带来性能提升才采纳”的原则,AVX2 决定不采纳,保留benchmark/bench_simd.cpp作 A/B 证据。
ACMTOrderBook/
├── dashboard.py ← 对比仪表盘源码
├── plot_throughput.py ← 版本演进吞吐曲线脚本
├── py/ ← Python 参照实现
│ ├── main.py
│ ├── behave/ ← 订单簿引擎核心
│ └── tool/ ← 消息解析工具
├── cpp v1/ ← 单线程 C++ 版本
├── cpp v2/ ← 多线程优化版本(跨平台, 当前引擎)
│ ├── main.cpp
│ ├── core/ ← 基础组件 (SPSC 队列、内存池、缓存行、CPU 亲和性、延迟统计)
│ ├── pipeline/ ← 管道架构 (生产者/消费者)
│ ├── behave/ ← 订单簿引擎核心
│ └── tool/ ← 消息解析工具 (mmap/field_parser)
├── benchmark/ ← SIMD A/B 基准 (bench_simd.cpp)
├── versions/ ← 历史版本源码归档 (v1 ~ v2.6)
├── throughput.png ← 版本演进实测曲线
└── data/ ← 测试数据 (体积大, 不入仓库, 见"数据源")
- 模块化:
axob_init/axob_order/axob_trade/axob_cage/axob_snap - 数据结构:价格档有序树 + 订单 O(1) 查表
- 精度处理:数据为交易所原生多尺度(深: 逐笔价格×10⁴/数量×10²/金额×10⁴,沪: 逐笔价格×10³/数量×10³/金额×10⁵;快照 111 主流价格 ×10⁶、昨收 ×10⁴ 特判)。引擎内部价格统一 ×10⁶(对齐官方快照)、全
int64、无__int128;金额 = 价格×数量,乘积落到交易所金额精度,境内单笔远低于 int64 上限。 - 创业板价格笼子:300xxx 股票 ±2% 有效竞价范围
- 交易阶段:OpenCall / AMTrading / PMTrading / CloseCall 全覆盖
- 关键修正:订单簿不从 orderMap 删除已成交订单(与原 Python 行为一致)
真实 L2 测试数据来自深交所行情,体积大、不入仓库(.gitignore 已排除 data/)。示例:
data/AX_sbe_szse_000001/AX_sbe_szse_000001.log
data/AX_sbe_szse_300750/AX_sbe_szse_300750.log
数据获取方式:历史 L2 样例见百度盘(提取码 rxif)。
