
面向低延迟交易系统的 C++
为什么最快的交易公司一切都用 C++ 来写
为什么 C++ 是速度之王
在对延迟敏感的技术栈里,C++ 依然牢牢占据着撮合路径:Globex 这类交易场所、许多股票行情源,以及加密引擎(包括 Hyperliquid 级别的基础设施),都会编译成可预测的机器码,中间不会藏着一次 GC(垃圾回收)停顿。
是什么让 C++ 在低延迟交易中独具优势?
- 没有垃圾回收器——没有不可预测的停顿。你能精确控制内存何时分配、何时释放。
- 贴近硬件——可直接访问 CPU 缓存行、SIMD 指令、内存映射 I/O,以及内核旁路(kernel bypass)网络。
- 编译期计算——模板元编程把工作从运行时挪到编译期,生成的代码快得堪比手写汇编。
- 可预测的延迟——只要编码足够讲究,你就能做到亚微秒级的 tick-to-trade(行情到下单)延迟,且抖动极小。
现实检验:在最快的交易席位上,tick-to-trade 的延迟预算往往是亚微秒级的。一次意外的内存分配或一次缓存未命中,就可能吃掉整个预算——所以团队会像守护生产流水线一样守护热路径(hot path)。
内存管理:栈、堆与自定义分配器
在低延迟 C++ 里,你如何分配内存,比你计算什么更重要。栈分配与堆分配在延迟上的差距可达 100 倍。
// Stack allocation — near-instant, deterministic
struct OrderUpdate {
uint64_t order_id;
double price;
uint32_t quantity;
char side; // 'B' or 'S'
};
void process_tick(const MarketData& tick) {
OrderUpdate update{}; // Stack — zero allocation cost
update.price = tick.mid_price();
// ... process on the hot path
}
// Heap allocation — slow, non-deterministic (avoid on hot path)
auto* order = new OrderUpdate{}; // Calls malloc — BAD on hot path
生产系统依赖自定义分配器,好让热路径永远不去调用通用的堆:
- 池分配器(Pool allocators)——预先分配一大块内存,再切出固定大小的小块。没有碎片,分配是 O(1)。
- 竞技场分配器(Arena allocators)——每次分配只把一个指针往前推进,最后一次性释放全部。非常适合逐条消息的处理。
- 大页(Huge pages)——2MB 或 1GB 的页能减少 TLB 未命中,当你的订单簿数据横跨数兆字节时尤为关键。
现代 C++ 借助 RAII(资源获取即初始化,Resource Acquisition Is Initialization)和智能指针,让安全的内存管理变得顺手好用:
// RAII — resource lifetime tied to scope
{
auto connection = std::make_unique<TcpConnection>(endpoint);
connection->send(order_message);
} // Connection automatically closed here
// Shared ownership for reference-counted resources
auto config = std::make_shared<TradingConfig>(load_config());
engine.set_config(config); // Multiple owners, automatic cleanup无锁数据结构与并发
互斥锁(mutex)是低延迟代码的敌人。即便没有竞争,一次 std::mutex::lock() 调用也要花 20-100 纳秒——而在有竞争的情况下,它可能让一个线程停顿数微秒。交易系统转而使用无锁(lock-free)数据结构。
交易中最关键的无锁结构是单生产者单消费者(SPSC)队列:
template<typename T, size_t Capacity>
class SPSCQueue {
alignas(64) std::array<T, Capacity> buffer_;
alignas(64) std::atomic<size_t> head_{0};
alignas(64) std::atomic<size_t> tail_{0};
public:
bool try_push(const T& item) {
const auto tail = tail_.load(std::memory_order_relaxed);
const auto next = (tail + 1) % Capacity;
if (next == head_.load(std::memory_order_acquire))
return false; // Full
buffer_[tail] = item;
tail_.store(next, std::memory_order_release);
return true;
}
bool try_pop(T& item) {
const auto head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_acquire))
return false; // Empty
item = buffer_[head];
head_.store((head + 1) % Capacity, std::memory_order_release);
return true;
}
};
关键的设计原则:
alignas(64)——让每个原子变量独占一个缓存行,防止 CPU 核心之间发生伪共享(false sharing)。- 内存序(Memory ordering)——
acquire/release语义比seq_cst更便宜,对生产者-消费者模式来说也已足够。 - 2 的幂大小——在生产环境里用 1024 或 4096 这类大小,取模就能变成一次按位与运算。
典型的接线方式:NIC(网卡)线程入队,策略线程出队——只要拓扑设计得当,整条传输路径上就没有互斥锁。
模板:编译期计算
C++ 模板让你把工作从运行时挪到编译期。在交易中,这意味着你的二进制文件会针对你交易的那些确切协议、品种和策略做特化——热路径上没有任何运行时分支。
// Compile-time FIX protocol field parsing
template<int Tag>
struct FIXField;
template<> struct FIXField<35> { // MsgType
static constexpr const char* name = "MsgType";
using type = char;
};
template<> struct FIXField<44> { // Price
static constexpr const char* name = "Price";
using type = double;
};
// Zero-overhead dispatch based on message type
template<char MsgType>
void handle_message(const char* raw, size_t len) {
if constexpr (MsgType == 'D') {
// New Order Single — inline at compile time
parse_new_order(raw, len);
} else if constexpr (MsgType == '8') {
// Execution Report
parse_execution(raw, len);
}
}
if constexpr 分支会在编译期被完全解析——生成的机器码只包含相关的那条路径,没有任何分支开销。把这一技巧与链接期优化(LTO)结合起来,能生成把协议解析基本展开成一连串直线式内存读取的二进制文件。
现代 C++20/23 的特性,例如 consteval、概念(concepts)和编译期容器,把这一点推得更远,让整条订单校验流水线都能在编译期算出来。
缓存友好的设计与内核旁路网络
在亚微秒级延迟下,CPU 缓存层级会成为你最重要的优化目标。一次到主存的缓存未命中要花约 100 纳秒——当你的总延迟预算只有 500ns 时,这简直是天长地久。
// BAD: Array of Pointers (AoP) — cache-hostile
std::vector<std::unique_ptr<Order>> orders; // Each access = pointer chase + cache miss
// GOOD: Struct of Arrays (SoA) — cache-friendly
struct OrderBook {
std::vector<double> prices; // Contiguous in memory
std::vector<uint32_t> quantities; // Contiguous in memory
std::vector<uint64_t> order_ids; // Contiguous in memory
};
// Iterating prices = sequential cache line reads = fast
在网络方面,Linux 内核的 TCP/IP 协议栈每个数据包会增加 5-15 微秒的延迟。交易公司会彻底绕过它:
- DPDK(数据平面开发套件,Data Plane Development Kit)——直接从用户态轮询 NIC,绕过内核。可实现亚微秒级的数据包处理。
- Solarflare OpenOnload——用熟悉的套接字 API 实现内核旁路。在股票和期货交易中被广泛使用。
- FPGA 网卡——Xilinx Alveo 及同类板卡能在硬件中解析行情并生成订单,实现纳秒级的延迟。
就连去中心化交易所也能从这些原则中受益。Hyperliquid 的 L1 链——为 GaiaEx 这类平台提供动力——在设计之初就考虑了高吞吐、低延迟的共识,连接它的做市商会使用经过优化的 C++ 客户端,以最大限度缩短从收到价格更新到提交订单之间的时间。
交易所撮合引擎是怎么搭出来的
每家交易所的核心都坐落着一个撮合引擎——负责配对买单和卖单的组件。它是整个金融领域里对延迟最敏感的软件,而且几乎总是用 C++ 写的。
一个简化版的撮合引擎架构:
class MatchingEngine {
// One order book per instrument
std::unordered_map<Symbol, OrderBook> books_;
void on_new_order(const Order& order) {
auto& book = books_[order.symbol];
auto matches = book.match(order);
for (const auto& fill : matches) {
publish_execution(fill); // To trading firms
update_market_data(fill); // To data feed
}
if (order.remaining_qty > 0) {
book.insert(order); // Rest on the book
}
}
};
生产级撮合引擎的优化远不止这副骨架:
- 价格-时间优先(Price-time priority)——同一价格上的订单按到达先后成交,用纳秒级时间戳来追踪。
- 预分配的订单池——撮合过程中不做堆分配。订单从固定的池里循环复用。
- 无锁设计——每个品种的订单簿跑在专属的核心上。无需跨订单簿加锁。
- 可确定性重放(Deterministic replay)——每一笔订单和每一次撮合都会记入持久化存储,以满足监管合规和灾难恢复的需要。
构建这种级别的系统,正是 C++ 真正大放异彩的地方。没有任何其他主流语言能同时给你对内存布局、线程调度、缓存行为和网络 I/O 的掌控——这正是超低延迟工程的四大支柱。无论你是在打造下一家交易所、作为做市商连接到某家交易所,还是在自营公司里优化执行,C++ 仍是无可争议的王者。