C++ 纳秒级交易系统设计


  • 罗马帝国 Hueman Contracts

    • 罗马人会在大型活动(如 Syracuse Maximus,可容纳 200,000-300,000 人)前一年,向农民、酿酒商等预定物资
    • 通过提前确定价格,发明了早期的衍生品交易
    • 目的:Reduce World Uncertainty
  • World Uncertainty Index

    • 该指数主要记录负面事件
    • 人类作为心理性生物,对潜在损失的权重高于潜在收益
    • 这解释了为什么人们难以安睡时,往往是担心可能亏损

1.2 现代市场制造(Market Making)

  • 定义:Market Maker 是在金融市场中提供流动性的角色,他们同时报出买入价(Bid)和卖出价(Ask)
  • 盈利模式 :通过买卖价差(Bid-Ask Spread)获取微薄利润
  • 特点 :Market Making is a Losers Game
    • 不是靠某个"银弹"打败市场
    • 需要持续地在所有方面都做得好
    • 小利润来自承担风险(optionality)的补偿
    • 风险不可免费,因为价格会波动

1.3 C++ 纳秒级交易系统设计

为什么需要低延迟编程?

类别 说明 示例
快速响应不确定事件 对突发新闻、市场变化做出即时反应 新闻发布后更新报价、取消订单
"聪明"地处理 包含优秀的模型、算法等智能决策 更好的交易策略、风险计算

第二部分:低延迟需求分析

2.1 高频交易中的延迟来源

在高频交易(HFT)系统中,延迟发生在多个环节:

复制代码
消息到达 → 网络接收 → 协议解析 → 订单簿更新 → 策略计算 → 订单生成 → 发送确认
    ↓
每个环节都可能成为瓶颈

2.2 延迟量级参考

延迟级别 典型耗时 说明
毫秒级 1-1000 ms 传统系统
微秒级 1-1000 μs 普通量化系统
纳秒级 1-1000 ns 高频交易前沿

2.3 交易系统中的参与者与延迟要求

不同市场参与者对延迟的要求不同:

参与者类型 延迟敏感度 主要策略
Market Maker 极高 被动提供流动性,依赖快速响应
Arbitrageur 极高 跨市场/跨品种套利
统计套利 基于模型的交易
基本面交易 中低 基于长期分析

第三部分:核心数据结构 - Order Book

订单簿是交易系统的核心数据结构,记录市场中所有的买卖订单:

  • Bid 侧:买家愿意购买的价格和数量
  • Ask 侧:卖家愿意出售的价格和数量
  • spread买卖价差 = Ask - Bid

3.2 操作

操作 说明
Add/Insert 添加新订单到订单簿
Remove/Cancel 取消已有订单
Modify 修改订单价格或数量
Match 价格匹配时成交

3.3 数据结构选择原则

高频交易系统选择订单簿数据结构时的考量:

  1. 查找速度:快速定位特定价格或订单
  2. 插入/删除效率:订单更新频繁
  3. 内存局部性:缓存友好
  4. 确定性延迟:避免最坏情况导致的延迟尖刺

第四部分:性能优化原则

4.1 核心工程原则

  1. Make It Work, Make It Right, Make It Fast

    • 先正确实现,再优化性能
  2. Measure Before Optimizing

    • 必须有性能数据支撑优化决策
  3. Optimize the Common Case

    • 关注最频繁的执行路径
  4. Embrace Hardware

    • 充分利用 CPU 硬件特性

4.2 CPU 硬件特性利用

4.2.1 CPU 缓存(Cache)
缓存层级 典型大小 访问延迟
L1 ~32 KB ~1 ns
L2 ~256 KB ~3-4 ns
L3 ~10-30 MB ~10-20 ns
主存 GB 级别 ~100 ns

优化策略

  • 数据对齐:确保数据结构与缓存行边界对齐
  • Prefetch:提前将数据加载到缓存
  • 数据布局优化 :使用 SoA(Structure of Arrays)替代 AoS(Array of Structures)
4.2.2 分支预测(Branch Prediction)
  • 现代 CPU 通过猜测分支方向来流水线执行
  • 错误预测代价:15-25 个时钟周期
  • 优化方法
    • 减少条件分支数量
    • 使用 likely/unlikely 提示
    • 考虑条件执行或查找表
4.2.3 指令级并行(ILP)
  • CPU 可以在同一时钟周期执行多条独立指令
  • 方法
    • 重排指令以增加并行度
    • 减少数据依赖
    • 展开循环

4.3 内存访问优化

4.3.1 NUMA(Non-Uniform Memory Access)

在多插槽服务器上:

  • 每个 CPU 插槽有本地内存
  • 访问远程内存有额外延迟
  • 优化:线程优先访问本地内存
4.3.2 内存分配策略
策略 优点 缺点
预分配池 无分配延迟、缓存友好 内存浪费
对象池 减少碎片 管理复杂
TLSF 等分配器 确定性分配时间 -

4.4 缓存优化技巧

数据结构设计

AoS vs SoA

cpp 复制代码
// AoS (Array of Structures) - 缓存不友好
struct Order {
    uint64_t price;
    uint32_t quantity;
    uint32_t timestamp;
};
Order orders[1000000];

// SoA (Structure of Arrays) - 缓存友好
struct OrderBook {
    uint64_t prices[1000000];
    uint32_t quantities[1000000];
    uint32_t timestamps[1000000];
};
向量化(Vectorization)
  • 利用 SIMD(Single Instruction Multiple Data)指令
  • SSE/AVX 可以在单条指令处理多个数据
  • 示例场景:批量价格计算、订单匹配
cpp 复制代码
// 伪代码示例:批量处理 4 个价格
__m256d prices = _mm256_load_pd(price_ptr);
__m256d result = _mm256_mul_pd(prices, multiplier);
_mm256_store_pd(result_ptr, result);

第五部分:Lock-Free Data Structures

5.1 为什么需要无锁结构?

对比项 有锁方案 无锁方案
延迟 可能阻塞等待 确定性等待
优先级反转 可能发生 避免
死锁 可能发生 避免
复杂度 相对简单 实现复杂

5.2 原子操作

操作 说明 CAS 用法
CAS Compare-And-Swap,比较并交换 最基础的无锁原语
FAA Fetch-And-Add,原子加法 计数器、序列号
Load/Store 原子读写 数据一致性

5.3 无锁订单簿设计考量

cpp 复制代码
// 简化的无锁订单指针更新示例
struct alignas(64) OrderNode {
    uint64_t price;
    uint32_t quantity;
    std::atomic<OrderNode*> next;
};

// 使用 CAS 更新链表
bool update_head(std::atomic<OrderNode*>& head, OrderNode* new_node) {
    OrderNode* old_head = head.load(std::memory_order_relaxed);
    new_node->next.store(old_head, std::memory_order_relaxed);
    return head.compare_exchange_weak(
        old_head, new_node,
        std::memory_order_release,
        std::memory_order_relaxed
    );
}

5.4 ABA 问题与解决方案

  • ABA 问题:线程 A 读取到值 A,切换;线程 B 将值改为 C 再改回 A;线程 A 继续认为值没变
  • 解决方案
    • 使用双宽 CAS(DCAS)存储指针+版本号
    • 使用带计数的指针(如 std::shared_ptr
    • 使用内存管理器的回收延迟

第六部分:多核并发处理

6.1 线程模型设计

高频交易系统典型线程模型:

模型 特点 适用场景
单线程事件循环 简单、无锁 单核心处理、IO 密集
Per-Symbol 线程 每个品种独立处理 多品种、低耦合
管道模型 阶段间流水线 复杂处理流程
混合模型 结合多种模型 实际生产系统

6.2 避免 False Sharing

False Sharing 发生在不同线程修改同一缓存行中的数据时:

cpp 复制代码
// 错误示例:False Sharing
struct {
    std::atomic<uint64_t> bid_price;      // 线程 1 修改
    std::atomic<uint64_t> ask_price;      // 线程 2 修改
    // 两者在同一缓存行,导致伪共享
} order_book;

// 正确示例:填充避免 False Sharing
struct {
    std::atomic<uint64_t> bid_price;      // 线程 1 修改
    char padding[64 - sizeof(uint64_t)];
    std::atomic<uint64_t> ask_price;      // 线程 2 修改
    char padding2[64 - sizeof(uint64_t)];
} order_book;

6.3 内存屏障(Memory Barriers)

屏障类型 说明 使用场景
Compiler Barrier 防止编译器重排 汇编内联
Hardware Barrier 防止 CPU 重排 多线程同步
Full Barrier 双向阻塞 重要同步点
Acquire 获取之前的加载 读取共享数据
Release 释放之后的存储 写入共享数据

第七部分:性能测量方法

7.1 测量的重要性

"If you can't measure it, you can't improve it."

性能优化的前提是准确的测量

7.2 测量工具与技术

7.2.1 硬件性能计数器(PMCs)

使用 perf 工具:

bash 复制代码
# 统计缓存命中率
perf stat -e cache-references,cache-misses ./trading_system

# 记录并分析
perf record -g ./trading_system
perf report
7.2.2 低延迟测量技术
技术 说明 精度
RDTSC Read Time-Stamp Counter,读取 CPU 时间戳计数器 纳秒级
RDTSCP RDTSC with Processor ID,防止乱序 纳秒级
Clock_gettime POSIX 时钟 纳秒级(CLOCK_MONOTONIC)
cpp 复制代码
// 使用 RDTSC 进行延迟测量
inline uint64_t get_time_ns() {
    unsigned int dummy;
    return __rdtscp(&dummy);
}

// 使用示例
uint64_t start = __rdtsc();
// ... 操作 ...
uint64_t end = __rdtsc();
uint64_t cycles = end - start;
double latency_ns = cycles / (CPU_FREQ_GHZ * 1000);
7.2.3 追踪与日志
  • 追踪(Tracing):记录每个操作的时序
  • 采样分析:低开销的 CPU 使用分析
  • 热路径分析:识别最频繁的执行路径

7.3 延迟分布分析

指标 说明 重要性
Mean 平均延迟 整体性能
Median (P50) 中位数 典型情况
P99 99 百分位 大多数请求
P999 99.9 百分位 尾部延迟
Max 最大延迟 极端情况

高频交易更关注 P999 而非平均值,因为一次极端延迟可能导致错过交易机会。

7.4 微基准测试(Microbenchmarks)

使用 Google Benchmark 库:

cpp 复制代码
#include <benchmark/benchmark.h>

static void BM_OrderBookUpdate(benchmark::State& state) {
    OrderBook book;
    for (auto _ : state) {
        book.update(100, 500);
    }
}
BENCHMARK(BM_OrderBookUpdate);

BENCHMARK_MAIN();

第八部分:C++ 语言特性在低延迟中的应用

8.1 编译期计算

技术 说明 示例
constexpr 编译期求值 常量计算
模板元编程 类型级计算 类型特化
if constexpr 编译期条件分支 减少分支

8.2 虚函数与内联

类型 优点 缺点
普通虚函数 运行时多态 间接调用、难以内联
模板 编译期多态 代码膨胀
CRTP 静多态、可内联 语法复杂
cpp 复制代码
// CRTP 示例
template<typename Derived>
class OrderBookBase {
public:
    void update() {
        // 静态派发,编译时内联
        static_cast<Derived*>(this)->update_impl();
    }
};

class FastOrderBook : public OrderBookBase<FastOrderBook> {
public:
    void update_impl() { /* 高效实现 */ }
};

8.3 内存管理

技术 开销 适用场景
栈分配 小对象、固定大小
对象池 最小 频繁创建销毁
arena/区域分配 同生命周期对象
智能指针 有一定开销 非性能关键路径

第九部分:建议

9.1 设计决策清单

在低延迟系统设计中需要考虑的问题:

  • 数据结构的时间复杂度是否满足需求?
  • 是否有不必要的锁竞争?
  • 缓存命中率如何?
  • 是否有 False Sharing?
  • 分支预测成功率如何?
  • 内存分配是否可预期?
  • 系统调用的频率?

原则

  1. 强调工程实践,关注实际问题
  2. 持续改进,不是一次性的银弹方案
  3. 全面优秀 :Market Making 的核心理念------持续在所有方面做得好
  4. 未雨绸缪 :像罗马人一样做好规划

技术栈

类别 工具/库 用途
性能分析 perf, VTune, Valgrind 性能分析
基准测试 Google Benchmark 微基准测试
并发 TBB, Folly, Seastar 并行库
网络 DPDK, Solarflare 低延迟网络
日志 spdlog, libfmt 高性能日志
  • 书籍
    • Advances in Financial Machine Learning - López de Prado
    • Quantitative Trading - Chan
    • C++ High Performance - Bjorn Andrist
  • 会议:CppCon, Meeting C++, ACCU
  • 论文:ACM SIGOPS, IEEE Transactions

9.2 反模式(Anti-Patterns)

反模式 问题 替代方案
全局锁 严重串行化 无锁结构或细粒度锁
不必要的虚函数 间接调用 CRTP、模板
过度抽象 代码复杂度 YAGNI 原则
忽略测量 盲目优化 先测量再优化
premature optimization 浪费时间 关注瓶颈

总结

  1. 市场制造的本质:需要持续在所有方面做到优秀
  2. 低延迟的两大驱动:快速响应 + 智能决策
  3. 优化方向
    • CPU 缓存友好设计
    • 无锁数据结构
    • 多核并发优化
    • 准确的性能测量
  4. 工程原则:测量优先、关注瓶颈、持续改进

高频交易系统的优化是一个系统性工程,需要在算法数据结构系统设计等多个层面协同优化。