GaiaEx AcademyGaiaEx Academy
C++ สำหรับระบบเทรดความหน่วงต่ำ
นักพัฒนาการเขียนโปรแกรม13 min read

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) เหมือนสายการผลิต

ทำไม hot path ยังอยู่ในโค้ดแบบเนทิฟ กำหนดแน่นอน (deterministic) • ไม่มี GC safepoint บนเส้นทาง • หน่วยความจำและ layout ชัดเจน • ควบคุม SIMD / cache • เข้ากับ kernel bypass ได้ดี วัดเป็น ns/µs • p99 > p50 สำคัญ • Jitter ทำลายความได้เปรียบด้าน co-location • ไบนารีที่เล่นซ้ำได้ • พูล/arena ขนาดคงที่ Interop • ผู้ผลิต NIC / FPGA ส่งมอบ C/C++ • DPDK, kernel module • ฟีด FIX / แบบไบนารี • ABI เดียวกันกับ OS
สแต็กของตลาดแลกเปลี่ยนให้ความสำคัญกับเลเทนซีที่คาดเดาได้และการเชื่อมต่อฮาร์ดแวร์แบบแน่นหนา—C++ คือเครื่องมือมาตรฐานสำหรับงานนี้

การจัดการหน่วยความจำ: 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
Stack เทียบกับ Heap บน hot path Stack / thread-local OrderUpdate บน stack — จำกัดขนาด, LIFO, cache-hot Pool / arena — ใช้ซ้ำแบบ O(1) ไม่มี malloc วุ่นวาย Heap (ทั่วไป) new/malloc — allocator lock, การแตกกระจาย เลเทนซีคาดเดาไม่ได้ — หลีกเลี่ยงต่อ tick
เก็บ struct ไว้บน stack หรือในพูลที่อุ่นไว้ล่วงหน้า ให้ถือว่าการชน heap เป็นบั๊กบน tick handler

โครงสร้างข้อมูลแบบ 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 บนเส้นทางสัญญาณถ้าคุณจัดโครงสร้างเครือข่ายได้ถูกต้อง

SPSC ring buffer (หนึ่งผู้เขียน หนึ่งผู้อ่าน) Producer feed / I/O thread Slot เป็นเลขยกกำลังสอง • atomic แบบ acquire/release Consumer strategy thread alignas(64) head/tail — แยก cache line เพื่อกำจัด false sharing Memory order: relaxed บน index ในเครื่อง, acquire/release ข้ามการส่งมอบ
หนึ่ง producer และหนึ่ง consumer ให้คุณข้าม mutex ได้ การจัดวางที่ถูกต้องทำให้คอร์ไม่แย่ง cache line เดียวกัน

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++ ยังคงเป็นราชาที่ไม่มีข้อกังขา