
面向低延遲交易系統的 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++ 仍是無可爭議的王者。