GaiaEx AcademyGaiaEx Academy
C++ для низьколатентних торгових систем
РозробникПрограмування13 min read

C++ для низьколатентних торгових систем

Чому найшвидші торгові фірми пишуть усе на C++

Поділитися

Чому C++ — це мова швидкості

У стеку, чутливому до латентності, C++ досі володіє шляхом зіставлення ордерів: майданчики у стилі Globex, багато фідів акцій і крипто-двигуни (включно з інфраструктурою класу Hyperliquid) компілюються в передбачуваний машинний код без пауз збирача сміття посередині.

Що робить C++ унікально придатним для низьколатентної торгівлі?

  • Немає збирача сміття — Немає непередбачуваних пауз. Ви точно контролюєте, коли пам’ять виділяється й звільняється.
  • Близькість до апаратного забезпечення — Прямий доступ до кеш-ліній процесора, SIMD-інструкцій, пам’яті, відображеної на I/O, і мережі з обходом ядра.
  • Обчислення на етапі компіляції — Метапрограмування шаблонів переносить роботу з часу виконання на час компіляції, виробляючи код, швидкий, як написаний руками асемблер.
  • Передбачувана латентність — З уважним кодуванням можна досягти субмікросекундної латентності tick-to-trade з мінімальним джитером.

Перевірка реальністю: Бюджети tick-to-trade на найшвидших деках часто субмікросекундні. Одне випадкове виділення пам’яті чи промах кешу може з’їсти весь бюджет — тому команди охороняють гарячий шлях, як виробничу лінію.

Чому гарячий шлях лишається в машинному коді Детермінованість • Без GC-зупинок на шляху • Явна пам’ять і розташування • Контроль SIMD / кешу • Сумісність з обходом ядра Вимірюється в нс/мкс • p99 важливіший, ніж p50 • Джитер знищує перевагу колокації • Відтворювані бінарники • Фіксовані пули / арени Сумісність • Постачальники NIC / FPGA дають C/C++ • DPDK, модулі ядра • Фіди FIX / бінарні фіди • Той самий ABI, що й ОС
Стеки бірж цінують передбачувану латентність і тісний зв’язок з апаратним забезпеченням — C++ є стандартним інструментом для цього завдання.

Керування пам’яттю: стек, купа й кастомні алокатори

У низьколатентному C++ те, як ви виділяєте пам’ять, важливіше, ніж те, що ви обчислюєте. Різниця між виділенням у стеку й у купі може становити 100x у латентності.

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

Продакшн-системи спираються на кастомні алокатори, щоб гарячий шлях ніколи не викликав загальну купу:

  • Пул-алокатори — Попередньо виділяють великий блок і вирізають фрагменти фіксованого розміру. Без фрагментації, O(1) виділення.
  • Арена-алокатори — Просувають вказівник уперед для кожного виділення, звільняють усе одразу. Ідеально для обробки за повідомленням.
  • Величезні сторінки (huge pages) — Сторінки на 2МБ чи 1ГБ зменшують промахи 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
Стек проти купи на гарячому шляху Стек / thread-local OrderUpdate у стеку — обмежено, LIFO, у кеші Пул / арена — O(1) повторне використання, без malloc Купа (загальна) new/malloc — блокування алокатора, фрагментація Непередбачувана латентність — уникати щотіка
Тримайте структури в стеку або в прогрітих пулах; ставтесь до звернень до купи як до багу в обробнику тіків.

Безблокувальні структури даних і конкурентність

М’ютекси — вороги низьколатентного коду. Один виклик 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) — Кожна атомарна змінна отримує власну кеш-лінію, запобігаючи фальшивому спільному використанню (false sharing) між ядрами процесора.
  • Порядок пам’яті — Семантика acquire/release дешевша за seq_cst і достатня для патернів виробник-споживач.
  • Розмір, що є степенем двійки — У продакшні використовуйте розміри як 1024 чи 4096, щоб операція модуля перетворювалась на побітове AND.

Типова розводка: потік NIC додає в черзу, потік стратегії читає з черги — без м’ютекса на дротовому шляху, якщо ви правильно вибудували топологію.

Кільцевий буфер SPSC (один писар, один читач) Виробник потік фіда / I/O Слоти-степені двійки • атомарні acquire/release Споживач потік стратегії alignas(64) head/tail — окремі кеш-лінії проти false sharing Порядок пам’яті: relaxed для локального індексу, acquire/release при передачі
Один виробник і один споживач дозволяють обійтись без м’ютексів; правильне вирівнювання утримує ядра від боротьби за одну кеш-лінію.

Шаблони: обчислення на етапі компіляції

Шаблони 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, концепти й контейнери часу компіляції, розширюють це ще далі, дозволяючи обчислювати цілі конвеєри валідації ордерів на етапі компіляції.

Кеш-орієнтований дизайн і мережа з обходом ядра

На субмікросекундних латентностях ієрархія кешу процесора стає вашою найважливішою ціллю оптимізації. Промах кешу до основної пам’яті коштує ~100 наносекунд — це вічність, коли загальний бюджет латентності — 500нс.

// 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 ядра Linux додає 5-15 мікросекунд латентності на пакет. Торгові фірми повністю його обходять:

  • DPDK (Data Plane Development Kit) — Опитує NIC безпосередньо з простору користувача, обходячи ядро. Досягає субмікросекундної обробки пакетів.
  • Solarflare OpenOnload — Обхід ядра зі звичним 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
        }
    }
};

Продакшн двигуни зіставлення оптимізують набагато більше цього скелета:

  • Пріоритет ціна-час — Ордери за однаковою ціною виконуються в порядку надходження, що відстежується за наносекундними мітками часу.
  • Попередньо виділені пули ордерів — Без виділення купи під час зіставлення. Ордери переробляються з фіксованих пулів.
  • Безблокувальний дизайн — Книга кожного інструмента працює на виділеному ядрі. Не потрібне блокування між книгами.
  • Детермінований повтор (replay) — Кожен ордер і зіставлення журналюється в постійне зберігання для регуляторної відповідності й відновлення після збою.

Побудова систем на цьому рівні — де C++ справді розкривається. Жодна інша мейнстрімна мова не дає одночасного контролю над розташуванням пам’яті, плануванням потоків, поведінкою кешу і мережевим I/O — чотирма стовпами інженерії з ультранизькою латентністю. Незалежно від того, чи будуєте наступну біржу, підключаєтесь до однієї як маркет-мейкер, чи оптимізуєте виконання в проп-фірмі, C++ залишається безсумнівним королем.