GaiaEx AcademyGaiaEx Academy
C++ cho hệ thống giao dịch độ trễ thấp
Lập Trình ViênLập Trình13 min read

C++ cho hệ thống giao dịch độ trễ thấp

Vì sao các công ty giao dịch nhanh nhất viết mọi thứ bằng C++

Chia Sẻ Bài Viết

Vì sao C++ là ngôn ngữ của tốc độ

Trong ngăn xếp nhạy cảm với độ trễ, C++ vẫn thống trị đường khớp lệnh: các nền tảng kiểu Globex, nhiều luồng dữ liệu cổ phiếu, và các engine crypto (bao gồm hạ tầng cấp Hyperliquid) biên dịch xuống mã máy có thể dự đoán được mà không có khoảng dừng GC ẩn ở giữa.

Điều gì làm C++ đặc biệt phù hợp cho giao dịch độ trễ thấp?

  • Không có garbage collector — Không có các khoảng dừng khó dự đoán. Bạn kiểm soát chính xác khi nào bộ nhớ được cấp phát và giải phóng.
  • Gần phần cứng (hardware proximity) — Truy cập trực tiếp vào các dòng cache CPU, lệnh SIMD, I/O ánh xạ bộ nhớ, và mạng bypass kernel.
  • Tính toán tại thời điểm biên dịch — Template metaprogramming chuyển công việc từ runtime sang thời điểm biên dịch, tạo ra code nhanh như assembly viết tay.
  • Độ trễ có thể dự đoán — Với việc viết code cẩn thận, bạn có thể đạt được độ trễ tick-to-trade dưới một micro giây với jitter tối thiểu.

Kiểm tra thực tế: Ngân sách tick-to-trade trên các bàn giao dịch nhanh nhất thường dưới một micro giây. Một lần cấp phát lạc hoặc cache miss có thể ăn hết toàn bộ ngân sách — vì vậy các đội bảo vệ đường hot path như một dây chuyền sản xuất.

Vì sao đường hot path vẫn ở mã native Xác định (deterministic) • Không có điểm dừng an toàn GC trên đường path • Bộ nhớ & bố cục rõ ràng • Kiểm soát SIMD / cache • Thân thiện với kernel bypass Đo bằng ns/µs • p99 > p50 mới quan trọng • Jitter giết chết lợi thế co-location • Binary có thể phát lại • Pool / arena cố định Tương thích (interop) • Nhà cung cấp NIC / FPGA xuất C/C++ • DPDK, module kernel • Luồng dữ liệu FIX / binary • Cùng ABI với OS
Các ngăn xếp sàn giao dịch đề cao độ trễ có thể dự đoán và sự gắn kết chặt với phần cứng — C++ là công cụ mặc định cho công việc đó.

Quản lý bộ nhớ: Stack, Heap, và Allocator tùy chỉnh

Trong C++ độ trễ thấp, cách bạn cấp phát bộ nhớ quan trọng hơn những gì bạn tính toán. Sự khác biệt giữa cấp phát stack và heap có thể là 100 lần về độ trễ.

// 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

Các hệ thống production dựa vào allocator tùy chỉnh để đường hot path không bao giờ gọi heap chung:

  • Pool allocator — Cấp phát trước một khối lớn và cắt ra các mảnh kích thước cố định. Không phân mảnh, cấp phát O(1).
  • Arena allocator — Đẩy con trỏ tiến lên cho mỗi lần cấp phát, giải phóng tất cả cùng một lúc. Hoàn hảo cho xử lý theo mỗi tin nhắn.
  • Trang khổng lồ (huge pages) — Trang 2MB hoặc 1GB giảm TLB miss, quan trọng khi dữ liệu sổ lệnh của bạn trải rộng megabyte.

C++ hiện đại làm cho việc quản lý bộ nhớ an toàn trở nên tiện lợi với RAII (Resource Acquisition Is Initialization) và 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 so với heap trên đường hot path Stack / thread-local OrderUpdate trên stack — có giới hạn, LIFO, cache-hot Pool / arena — tái sử dụng O(1), không có xáo trộn malloc Heap (chung) new/malloc — khóa allocator, phân mảnh Độ trễ khó dự đoán — tránh dùng mỗi tick
Giữ struct trên stack hoặc trong các pool đã làm nóng trước; coi các lần chạm heap là lỗi trên tick handler.

Cấu trúc dữ liệu lock-free và concurrency

Mutex là kẻ thù của code độ trễ thấp. Một cuộc gọi std::mutex::lock() duy nhất có thể tốn 20-100 nano giây dù không có tranh chấp — và dưới tranh chấp, nó có thể làm một luồng dừng lại trong micro giây. Các hệ thống giao dịch sử dụng cấu trúc dữ liệu lock-free thay thế.

Cấu trúc lock-free quan trọng nhất trong giao dịch là hàng đợi Single-Producer Single-Consumer (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;
    }
};

Các nguyên lý thiết kế chính:

  • alignas(64) — Mỗi biến atomic nhận dòng cache riêng của nó, ngăn false sharing giữa các core CPU.
  • Thứ tự bộ nhớ (memory ordering) — Ngữ nghĩa acquire/release rẻ hơn seq_cst và đủ cho các mẫu producer-consumer.
  • Kích thước theo lũy thừa của hai — Trong production, dùng các kích thước như 1024 hoặc 4096 để phép modulo trở thành một phép AND bitwise.

Cách nối dây thông thường: luồng NIC đẩy vào (enqueue), luồng chiến lược lấy ra (dequeue) — không có mutex trên đường dây nếu bạn dùng đúng cấu trúc mạng.

Ring buffer SPSC (một luồng viết, một luồng đọc) Producer luồng feed / I/O Các slot theo lũy thừa của hai • atomic acquire/release Consumer luồng chiến lược alignas(64) head/tail — dòng cache riêng để loại bỏ false sharing Thứ tự bộ nhớ: relaxed trên chỉ số cục bộ, acquire/release qua handoff
Một producer và một consumer cho phép bạn bỏ qua mutex; căn chỉnh đúng giữ các core không tranh nhau dòng cache giống nhau.

Template: tính toán tại thời điểm biên dịch

Template C++ cho phép bạn chuyển công việc từ runtime sang thời điểm biên dịch. Trong giao dịch, điều này nghĩa là binary của bạn được chuyên biệt hóa cho chính xác các giao thức, công cụ, và chiến lược bạn giao dịch — không có phân nhánh runtime trên đường 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);
    }
}

Các nhánh if constexpr được giải quyết hoàn toàn tại thời điểm biên dịch — mã máy được tạo ra chỉ chứa đường path liên quan với zero chi phí phân nhánh. Kỹ thuật này, kết hợp với tối ưu hóa thời điểm link (LTO), tạo ra các binary nơi việc phân tích giao thức thực chất được unroll thành một chuỗi tuyến tính các lần đọc bộ nhớ.

Các tính năng C++20/23 hiện đại như consteval, concept, và container thời điểm biên dịch đẩy điều này xa hơn nữa, cho phép toàn bộ pipeline xác thực lệnh được tính toán tại thời điểm biên dịch.

Thiết kế thân thiện với cache và mạng kernel bypass

Ở độ trễ dưới micro giây, hệ thống phân cấp cache CPU trở thành mục tiêu tối ưu hóa quan trọng nhất của bạn. Một cache miss đến bộ nhớ chính tốn ~100 nano giây — đó là một sự vĩnh cửu khi tổng ngân sách độ trễ của bạn là 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

Đối với mạng, ngăn xếp TCP/IP của kernel Linux thêm 5-15 micro giây độ trễ mỗi packet. Các công ty giao dịch bypass nó hoàn toàn:

  • DPDK (Data Plane Development Kit) — Poll trực tiếp NIC từ userspace, bypass kernel. Đạt được xử lý packet dưới micro giây.
  • Solarflare OpenOnload — Kernel bypass với một API socket quen thuộc. Được sử dụng rộng rãi trong giao dịch cổ phiếu và tương lai.
  • NIC FPGA — Các card Xilinx Alveo và tương tự có thể phân tích dữ liệu thị trường và tạo lệnh trong phần cứng, đạt được độ trễ cấp độ nano giây.

Ngay cả các sàn giao dịch phi tập trung cũng được lợi từ những nguyên lý này. Chain L1 của Hyperliquid — thứ cấp năng lượng cho các nền tảng như GaiaEx — được thiết kế với đồng thuận thông lượng cao, độ trễ thấp trong tư tưởng, và các market maker kết nối với nó sử dụng các client C++ được tối ưu hóa để giảm thiểu thời gian giữa việc nhận một cập nhật giá và gửi một lệnh.

Cách các engine khớp lệnh của sàn giao dịch được xây dựng

Ở trung tâm của mọi sàn giao dịch là engine khớp lệnh (matching engine) — thành phần ghép các lệnh mua và bán. Đây là phần mềm nhạy cảm với độ trễ nhất trong toàn bộ tài chính, và nó hầu như luôn được viết bằng C++.

Một kiến trúc engine khớp lệnh đơn giản hóa:

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
        }
    }
};

Các engine khớp lệnh production tối ưu hóa xa hơn nhiều bộ khung này:

  • Ưu tiên giá-thời gian (price-time priority) — Các lệnh cùng giá được khớp theo thứ tự đến, theo dõi bằng timestamp nano giây.
  • Pool lệnh cấp phát trước — Không có cấp phát heap trong khi khớp lệnh. Các lệnh được tái sử dụng từ các pool cố định.
  • Thiết kế không khóa (lockless) — Sổ lệnh của mỗi công cụ chạy trên một core dành riêng. Không cần khóa chéo sổ.
  • Phát lại xác định (deterministic replay) — Mọi lệnh và khớp lệnh được ghi vào lưu trữ bền vững cho tuân thủ quy định và khôi phục sau thảm họa.

Xây dựng các hệ thống ở cấp độ này là nơi C++ thực sự tỏa sáng. Không ngôn ngữ chủ đạo khác cho bạn quyền kiểm soát đồng thời trên bố cục bộ nhớ, lập lịch luồng, hành vi cache, và I/O mạng — bốn trụ cột của kỹ thuật độ trễ siêu thấp. Cho dù bạn đang xây dựng sàn giao dịch tiếp theo, kết nối với một sàn như một market maker, hoặc tối ưu hóa thực thi tại một prop firm, C++ vẫn là vua không thể tranh cãi.