GaiaEx AcademyGaiaEx Academy
C++ per sistemi di trading a bassa latenza
SviluppatoreProgrammazione13 min read

C++ per sistemi di trading a bassa latenza

Perché le trading firm più veloci scrivono tutto in C++

Condividi Post

Perché il C++ è il linguaggio della velocità

Nello stack sensibile alla latenza, il C++ domina ancora il percorso di matching: piattaforme in stile Globex, molti feed azionari e motori crypto (inclusa l'infrastruttura di classe Hyperliquid) compilano codice macchina prevedibile senza alcuna pausa da garbage collector nascosta nel mezzo.

Cosa rende il C++ unicamente adatto al trading a bassa latenza?

  • Nessun garbage collector — Nessuna pausa imprevedibile. Controlli esattamente quando la memoria viene allocata e liberata.
  • Prossimità all'hardware — Accesso diretto alle cache line della CPU, istruzioni SIMD, I/O mappato in memoria e networking con kernel bypass.
  • Calcolo a tempo di compilazione — Il template metaprogramming sposta il lavoro dal runtime al tempo di compilazione, producendo codice tanto rapido quanto assembly scritto a mano.
  • Latenza prevedibile — Con una programmazione attenta, puoi raggiungere una latenza tick-to-trade sub-microsecondo con jitter minimo.

Verifica della realtà: i budget tick-to-trade nei desk più veloci sono spesso sub-microsecondo. Un'allocazione fuori posto o un cache miss possono consumare l'intero budget — così i team proteggono il percorso critico (hot path) come una linea di produzione.

Perché il percorso critico resta in codice nativo Deterministico • Nessun safepoint GC sul percorso • Memoria & layout espliciti • Controllo SIMD / cache • Compatibile con kernel bypass Misurato in ns/µs • p99 > p50 conta • Il jitter uccide il vantaggio di co-location • Binari riproducibili • Pool / arene fisse Interoperabilità • I produttori di NIC / FPGA forniscono C/C++ • DPDK, moduli kernel • Feed FIX / binari • Stessa ABI del sistema operativo
Gli stack degli exchange premiano la latenza prevedibile e un accoppiamento stretto con l'hardware — il C++ è lo strumento predefinito per questo compito.

Gestione della memoria: stack, heap e allocatori personalizzati

Nel C++ a bassa latenza, come alloca la memoria conta più di cosa calcola. La differenza tra allocazione su stack e su heap può essere 100 volte in termini di latenza.

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

I sistemi in produzione si basano su allocatori personalizzati così che il percorso critico non chiami mai l'heap generico:

  • Pool allocator — Pre-allocano un grande blocco e ne ritagliano porzioni di dimensione fissa. Nessuna frammentazione, allocazione O(1).
  • Arena allocator — Fanno avanzare un puntatore per ogni allocazione, liberando tutto in una volta. Perfetti per l'elaborazione per-messaggio.
  • Huge pages — Pagine da 2MB o 1GB riducono i miss della TLB, cruciale quando i dati del tuo order book si estendono su megabyte.

Il C++ moderno rende la gestione sicura della memoria ergonomica con RAII (Resource Acquisition Is Initialization) e 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 vs heap sul percorso critico Stack / thread-local OrderUpdate sullo stack — limitato, LIFO, cache-hot Pool / arena — riuso O(1), nessun churn del malloc Heap (generico) new/malloc — lock dell'allocatore, frammentazione Latenza imprevedibile — evitare a ogni tick
Tieni le struct sullo stack o in pool pre-riscaldati; considera gli accessi all'heap come bug nel gestore dei tick.

Strutture dati lock-free e concorrenza

I mutex sono il nemico del codice a bassa latenza. Una singola chiamata a std::mutex::lock() può richiedere 20-100 nanosecondi anche senza contesa — e sotto contesa, può bloccare un thread per microsecondi. I sistemi di trading usano invece strutture dati lock-free.

La struttura lock-free più critica nel trading è la coda 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;
    }
};

Principi di design chiave:

  • alignas(64) — Ogni variabile atomica ottiene la propria cache line, prevenendo il false sharing tra i core della CPU.
  • Ordinamento della memoria — La semantica acquire/release è più economica di seq_cst e sufficiente per i pattern produttore-consumatore.
  • Dimensionamento a potenza di due — In produzione, usa dimensioni come 1024 o 4096 così che il modulo diventi un AND bit a bit.

Cablaggio tipico: il thread della NIC accoda, il thread della strategia estrae — nessun mutex sul percorso del cavo se ottieni la topologia giusta.

Ring buffer SPSC (un writer, un reader) Produttore thread feed / I/O Slot a potenza di due • atomiche acquire/release Consumatore thread strategia alignas(64) head/tail — cache line separate per eliminare il false sharing Ordinamento memoria: relaxed sull'indice locale, acquire/release nel passaggio
Un produttore e un consumatore permettono di evitare i mutex; l'allineamento corretto impedisce ai core di contendersi la stessa cache line.

Template: calcolo a tempo di compilazione

I template del C++ permettono di spostare il lavoro dal runtime al tempo di compilazione. Nel trading, questo significa che il tuo binario è specializzato esattamente per i protocolli, gli strumenti e le strategie che scambi — nessun branching a runtime sul percorso critico.

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

I rami if constexpr vengono risolti interamente a tempo di compilazione — il codice macchina generato contiene solo il percorso rilevante con zero overhead di branching. Questa tecnica, combinata con l'ottimizzazione al link-time (LTO), produce binari in cui il parsing del protocollo è essenzialmente srotolato in una sequenza lineare di letture di memoria.

Le funzionalità moderne del C++20/23 come consteval, i concept e i contenitori a tempo di compilazione spingono questo concetto ancora più in là, permettendo di calcolare intere pipeline di validazione degli ordini a tempo di compilazione.

Design cache-friendly e networking con kernel bypass

A latenze sub-microsecondo, la gerarchia della cache della CPU diventa il tuo obiettivo di ottimizzazione più importante. Un cache miss verso la memoria principale costa ~100 nanosecondi — un'eternità quando il tuo budget di latenza totale è di 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

Per il networking, lo stack TCP/IP del kernel Linux aggiunge 5-15 microsecondi di latenza per pacchetto. Le aziende di trading lo evitano completamente:

  • DPDK (Data Plane Development Kit) — Interroga la NIC direttamente dallo userspace, evitando il kernel. Raggiunge un'elaborazione dei pacchetti sub-microsecondo.
  • Solarflare OpenOnload — Kernel bypass con un'API socket familiare. Usato ampiamente nel trading di azioni e futures.
  • NIC FPGA — Schede come Xilinx Alveo e simili possono analizzare i dati di mercato e generare ordini in hardware, raggiungendo latenze a livello di nanosecondi.

Anche gli exchange decentralizzati beneficiano di questi principi. La catena L1 di Hyperliquid — che alimenta piattaforme come GaiaEx — è stata progettata pensando a un consenso ad alto throughput e bassa latenza, e i market maker che si connettono a essa usano client C++ ottimizzati per minimizzare il tempo tra la ricezione di un aggiornamento di prezzo e l'invio di un ordine.

Come sono costruiti i motori di matching degli exchange

Al centro di ogni exchange si trova il motore di matching — il componente che abbina gli ordini di acquisto e vendita. È il pezzo di software più sensibile alla latenza in tutta la finanza, e viene quasi sempre scritto in C++.

Un'architettura semplificata di un motore di matching:

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

I motori di matching in produzione si ottimizzano ben oltre questo scheletro:

  • Priorità prezzo-tempo — Gli ordini allo stesso prezzo vengono eseguiti nell'ordine di arrivo, tracciato con timestamp in nanosecondi.
  • Pool di ordini pre-allocati — Nessuna allocazione heap durante il matching. Gli ordini vengono riciclati da pool fissi.
  • Design senza lock — L'order book di ogni strumento gira su un core dedicato. Non serve alcun lock tra order book diversi.
  • Replay deterministico — Ogni ordine e ogni match viene registrato su storage persistente per la conformità regolatoria e il disaster recovery.

Costruire sistemi a questo livello è dove il C++ brilla davvero. Nessun altro linguaggio mainstream ti dà controllo simultaneo su layout della memoria, scheduling dei thread, comportamento della cache e I/O di rete — i quattro pilastri dell'engineering a bassissima latenza. Sia che tu stia costruendo il prossimo exchange, connettendoti a uno come market maker, o ottimizzando l'esecuzione in una prop firm, il C++ resta il re indiscusso.