
C++ สำหรับระบบเทรดความหน่วงต่ำ
ทำไมบริษัทเทรดที่เร็วที่สุดเขียนทุกอย่างด้วย C++
ทำไม C++ คือภาษาแห่งความเร็ว
ในสแต็กที่ใส่ใจเลเทนซี C++ ยังคงครองเส้นทางการจับคู่คำสั่ง: ตลาดสไตล์ Globex ฟีดหุ้นหลายแห่ง และเอนจินคริปโต (รวมถึงโครงสร้างพื้นฐานระดับ Hyperliquid) คอมไพล์ลงเป็นโค้ดเครื่องที่คาดเดาได้โดยไม่มีการหยุดชั่วคราวของ garbage collector (GC) ซ่อนอยู่ตรงกลาง
อะไรทำให้ C++ เหมาะกับการเทรดแบบเลเทนซีต่ำโดยเฉพาะ?
- ไม่มี garbage collector — ไม่มีการหยุดชั่วคราวที่คาดเดาไม่ได้ คุณควบคุมได้อย่างเจาะจงว่าเมื่อไหร่หน่วยความจำจะถูกจัดสรรและปลดปล่อย
- ความใกล้ชิดกับฮาร์ดแวร์ — เข้าถึงตรงกับ cache line ของ CPU คำสั่ง SIMD I/O แบบ memory-mapped และเครือข่ายแบบ kernel bypass
- การคำนวณตอนคอมไพล์ — Template metaprogramming ย้ายงานจาก runtime ไปยังตอนคอมไพล์ ผลิตโค้ดที่เร็วเท่ากับ assembly ที่เขียนด้วยมือ
- เลเทนซีที่คาดเดาได้ — ด้วยการเขียนโค้ดอย่างระมัดระวัง คุณสามารถบรรลุเลเทนซี tick-to-trade ระดับต่ำกว่าไมโครวินาทีด้วย jitter ที่น้อยที่สุด
ตรวจสอบความเป็นจริง: งบเลเทนซี tick-to-trade บนโต๊ะเทรดที่เร็วที่สุดมักต่ำกว่าไมโครวินาที การจัดสรรหน่วยความจำที่หลุดออกไปครั้งเดียวหรือ cache miss ครั้งเดียวสามารถกินงบทั้งหมดได้—ดังนั้นทีมงานจึงเฝ้าระวังเส้นทางที่วิ่งบ่อยที่สุด (hot path) เหมือนสายการผลิต
การจัดการหน่วยความจำ: Stack, Heap และ Custom Allocator
ใน C++ แบบเลเทนซีต่ำ วิธีที่คุณจัดสรรหน่วยความจำสำคัญกว่าสิ่งที่คุณคำนวณ ความแตกต่างระหว่างการจัดสรรบน stack และ heap อาจต่างกัน 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
ระบบระดับผลิตพึ่งพา custom allocator เพื่อให้ hot path ไม่เคยเรียก heap ทั่วไป:
- Pool allocator — จัดสรรบล็อกขนาดใหญ่ล่วงหน้าและแบ่งเป็นชิ้นขนาดคงที่ ไม่มีการแตกกระจาย (fragmentation) การจัดสรรเป็น O(1)
- Arena allocator — ขยับตัวชี้ไปข้างหน้าสำหรับการจัดสรรแต่ละครั้ง ปลดปล่อยทั้งหมดในครั้งเดียว เหมาะที่สุดสำหรับการประมวลผลแบบต่อข้อความ
- Huge page — เพจขนาด 2MB หรือ 1GB ลด TLB miss สำคัญมากเมื่อข้อมูลออเดอร์บุ๊กของคุณมีขนาดหลายเมกะไบต์
C++ สมัยใหม่ทำให้การจัดการหน่วยความจำที่ปลอดภัยใช้งานง่ายขึ้นด้วย RAII (Resource Acquisition Is Initialization) และ smart pointer:
// 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โครงสร้างข้อมูลแบบ Lock-Free และการทำงานพร้อมกัน
Mutex เป็นศัตรูของโค้ดเลเทนซีต่ำ การเรียก std::mutex::lock() เพียงครั้งเดียวอาจใช้เวลา 20-100 นาโนวินาทีแม้ไม่มีการแย่งกัน—และเมื่อมีการแย่งกัน มันอาจทำให้เธรดหยุดชะงักเป็นไมโครวินาที ระบบเทรดใช้โครงสร้างข้อมูลแบบ lock-free แทน
โครงสร้าง lock-free ที่สำคัญที่สุดในการเทรดคือ Single-Producer Single-Consumer (SPSC) queue:
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)— ตัวแปร atomic แต่ละตัวได้ cache line ของตัวเอง ป้องกัน false sharing ระหว่างคอร์ CPU- Memory ordering — ความหมายของ
acquire/releaseมีค่าใช้จ่ายต่ำกว่าseq_cstและเพียงพอสำหรับรูปแบบ producer-consumer - ขนาดเป็นเลขยกกำลังสอง — ในระบบผลิต ใช้ขนาดอย่าง 1024 หรือ 4096 เพื่อให้ modulo กลายเป็น bitwise AND
การเชื่อมต่อโดยทั่วไป: เธรด NIC เข้าคิว เธรดกลยุทธ์ออกจากคิว—ไม่มี mutex บนเส้นทางสัญญาณถ้าคุณจัดโครงสร้างเครือข่ายได้ถูกต้อง
Template: การคำนวณตอนคอมไพล์
Template ของ C++ ให้คุณย้ายงานจาก runtime ไปเป็นตอนคอมไพล์ ในการเทรด นี่หมายความว่าไบนารีของคุณถูกปรับเฉพาะสำหรับโปรโตคอล ตราสาร และกลยุทธ์ที่คุณเทรดพอดี—ไม่มีการแตกแขนง (branching) ที่ runtime บน hot path
// 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 concept และคอนเทนเนอร์ตอนคอมไพล์ผลักดันสิ่งนี้ไปได้ไกลกว่าเดิม เปิดให้ท่อประมวลผลตรวจสอบคำสั่งทั้งชุดถูกคำนวณตอนคอมไพล์ได้
การออกแบบที่เป็นมิตรกับ Cache และเครือข่ายแบบ Kernel Bypass
ที่เลเทนซีระดับต่ำกว่าไมโครวินาที ลำดับชั้น cache ของ CPU กลายเป็นเป้าหมายการปรับให้เหมาะสมที่สำคัญที่สุดของคุณ cache miss ที่ต้องไปหน่วยความจำหลักมีค่าใช้จ่ายประมาณ 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
สำหรับเครือข่าย สแต็ก TCP/IP ของ kernel Linux เพิ่มเลเทนซี 5-15 ไมโครวินาทีต่อแพ็กเก็ต บริษัทเทรดข้ามมันไปเลยทั้งหมด:
- DPDK (Data Plane Development Kit) — โพลล์ NIC โดยตรงจาก userspace ข้าม kernel ไป บรรลุการประมวลผลแพ็กเก็ตระดับต่ำกว่าไมโครวินาที
- Solarflare OpenOnload — Kernel bypass ด้วย socket API ที่คุ้นเคย ใช้กันอย่างกว้างขวางในการเทรดหุ้นและฟิวเจอร์ส
- FPGA NIC — การ์ดอย่าง Xilinx Alveo และการ์ดที่คล้ายกันสามารถแปลงข้อมูลตลาดและสร้างคำสั่งในฮาร์ดแวร์ บรรลุเลเทนซีระดับนาโนวินาที
แม้แต่ตลาดแลกเปลี่ยนแบบกระจายศูนย์ก็ได้ประโยชน์จากหลักการเหล่านี้ เชน L1 ของ Hyperliquid—ซึ่งขับเคลื่อนแพลตฟอร์มอย่าง GaiaEx—ถูกออกแบบโดยคำนึงถึงฉันทามติแบบปริมาณสูงและเลเทนซีต่ำ และมาร์เก็ตเมกเกอร์ที่เชื่อมต่อกับมันใช้ไคลเอนต์ C++ ที่ปรับให้เหมาะสมเพื่อลดเวลาระหว่างการรับข้อมูลอัปเดตราคากับการส่งคำสั่ง
เครื่องจับคู่คำสั่งของตลาดแลกเปลี่ยนถูกสร้างขึ้นอย่างไร
ที่ใจกลางของตลาดแลกเปลี่ยนทุกแห่งคือ เครื่องจับคู่คำสั่ง (matching engine)—ส่วนประกอบที่จับคู่คำสั่งซื้อและขาย มันคือซอฟต์แวร์ที่ใส่ใจเลเทนซีมากที่สุดในโลกการเงินทั้งหมด และมันมักถูกเขียนด้วย 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) — คำสั่งที่ราคาเดียวกันถูกเติมตามลำดับที่มาถึง ติดตามด้วย timestamp ระดับนาโนวินาที
- พูลคำสั่งที่จัดสรรไว้ล่วงหน้า — ไม่มีการจัดสรร heap ระหว่างการจับคู่ คำสั่งถูกนำมาใช้ซ้ำจากพูลขนาดคงที่
- การออกแบบแบบไม่มี lock — ออเดอร์บุ๊กของแต่ละตราสารรันบนคอร์เฉพาะ ไม่ต้อง lock ข้ามบุ๊ก
- การเล่นซ้ำแบบกำหนดแน่นอน — คำสั่งและการจับคู่ทุกรายการถูกบันทึกลงในที่จัดเก็บถาวรเพื่อการปฏิบัติตามกฎระเบียบและการกู้คืนจากภัยพิบัติ
การสร้างระบบในระดับนี้คือจุดที่ C++ ฉายแสงจริง ๆ ไม่มีภาษากระแสหลักอื่นใดที่ให้คุณควบคุม layout หน่วยความจำ การจัดตารางเธรด พฤติกรรม cache และ network I/O พร้อมกัน—สี่เสาหลักของวิศวกรรมเลเทนซีต่ำสุดขั้ว ไม่ว่าคุณจะสร้างตลาดแลกเปลี่ยนแห่งต่อไป เชื่อมต่อกับมันในฐานะมาร์เก็ตเมกเกอร์ หรือปรับการส่งคำสั่งให้เหมาะสมที่บริษัทเทรดกรรมสิทธิ์ C++ ยังคงเป็นราชาที่ไม่มีข้อกังขา