
C++ для низьколатентних торгових систем
Чому найшвидші торгові фірми пишуть усе на C++
Чому C++ — це мова швидкості
У стеку, чутливому до латентності, C++ досі володіє шляхом зіставлення ордерів: майданчики у стилі Globex, багато фідів акцій і крипто-двигуни (включно з інфраструктурою класу Hyperliquid) компілюються в передбачуваний машинний код без пауз збирача сміття посередині.
Що робить C++ унікально придатним для низьколатентної торгівлі?
- Немає збирача сміття — Немає непередбачуваних пауз. Ви точно контролюєте, коли пам’ять виділяється й звільняється.
- Близькість до апаратного забезпечення — Прямий доступ до кеш-ліній процесора, SIMD-інструкцій, пам’яті, відображеної на I/O, і мережі з обходом ядра.
- Обчислення на етапі компіляції — Метапрограмування шаблонів переносить роботу з часу виконання на час компіляції, виробляючи код, швидкий, як написаний руками асемблер.
- Передбачувана латентність — З уважним кодуванням можна досягти субмікросекундної латентності tick-to-trade з мінімальним джитером.
Перевірка реальністю: Бюджети tick-to-trade на найшвидших деках часто субмікросекундні. Одне випадкове виділення пам’яті чи промах кешу може з’їсти весь бюджет — тому команди охороняють гарячий шлях, як виробничу лінію.
Керування пам’яттю: стек, купа й кастомні алокатори
У низьколатентному 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Безблокувальні структури даних і конкурентність
М’ютекси — вороги низьколатентного коду. Один виклик 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 додає в черзу, потік стратегії читає з черги — без м’ютекса на дротовому шляху, якщо ви правильно вибудували топологію.
Шаблони: обчислення на етапі компіляції
Шаблони 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++ залишається безсумнівним королем.