GaiaEx AcademyGaiaEx Academy
C++ für Low-Latency-Trading-Systeme
EntwicklerProgrammierung13 min read

C++ für Low-Latency-Trading-Systeme

Warum die schnellsten Trading-Firmen alles in C++ schreiben

Beiträge teilen

Warum C++ die Sprache der Geschwindigkeit ist

Im latenzsensitiven Stack besitzt C++ nach wie vor den Matching-Pfad: Plattformen im Globex-Stil, viele Aktien-Feeds und Krypto-Engines (einschließlich Infrastruktur der Hyperliquid-Klasse) kompilieren zu vorhersagbarem Maschinencode, ohne dass sich mittendrin eine GC-Pause verbirgt.

Was macht C++ einzigartig geeignet für latenzarmes Trading?

  • Kein Garbage Collector — Keine unvorhersehbaren Pausen. Du kontrollierst exakt, wann Speicher zugewiesen und freigegeben wird.
  • Hardwarenähe — direkter Zugriff auf CPU-Cache-Lines, SIMD-Befehle, Memory-Mapped I/O und Kernel-Bypass-Networking.
  • Compile-Zeit-Berechnung — Template-Metaprogrammierung verschiebt Arbeit von der Laufzeit auf die Compile-Zeit und erzeugt Code, der so schnell ist wie handgeschriebenes Assembly.
  • Vorhersagbare Latenz — Mit sorgfältiger Programmierung erreichst du eine Tick-to-Trade-Latenz im Sub-Mikrosekunden-Bereich mit minimalem Jitter.

Realitätscheck: Tick-to-Trade-Budgets bei den schnellsten Desks liegen oft im Sub-Mikrosekunden-Bereich. Eine einzige verirrte Allokation oder ein Cache-Miss kann das gesamte Budget verbrauchen — deshalb bewachen Teams den Hot Path wie eine Fertigungsstraße.

Warum der Hot Path im nativen Code bleibt Deterministisch • Keine GC-Safepoints im Pfad • Explizite Speicherung & Layout • SIMD-/Cache-Kontrolle • Kernel-Bypass-freundlich Gemessen in ns/µs • p99 > p50 ist entscheidend • Jitter zerstört Co-Location-Vorteil • Wiederholbare Binaries • Feste Pools / Arenen Interop • NIC-/FPGA-Hersteller liefern C/C++ • DPDK, Kernel-Module • FIX-/Binärfeeds • Gleiche ABI wie das OS
Börsen-Stacks legen Wert auf vorhersagbare Latenz und enge Hardware-Kopplung — C++ ist das Standardwerkzeug für diese Aufgabe.

Speicherverwaltung: Stack, Heap und benutzerdefinierte Allokatoren

In latenzarmem C++ zählt wie du Speicher zuweist mehr als was du berechnest. Der Unterschied zwischen Stack- und Heap-Allokation kann in der Latenz das 100-Fache betragen.

// Stack-Allokation — nahezu sofort, deterministisch
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-Allokation — langsam, nicht deterministisch (auf dem Hot Path vermeiden)
auto* order = new OrderUpdate{};  // Calls malloc — BAD on hot path

Produktionssysteme setzen auf benutzerdefinierte Allokatoren, damit der Hot Path niemals den generischen Heap aufruft:

  • Pool-Allokatoren — Reservieren einen großen Block vorab und schneiden feste Chunks heraus. Keine Fragmentierung, O(1)-Allokation.
  • Arena-Allokatoren — Schieben bei jeder Allokation einen Zeiger vorwärts, geben alles auf einmal frei. Perfekt für die Verarbeitung pro Nachricht.
  • Huge Pages — 2-MB- oder 1-GB-Seiten reduzieren TLB-Misses, entscheidend, wenn deine Orderbuchdaten sich über Megabytes erstrecken.

Modernes C++ macht sichere Speicherverwaltung ergonomisch mit RAII (Resource Acquisition Is Initialization) und Smart Pointers:

// RAII — Lebensdauer der Ressource an den Gültigkeitsbereich gebunden
{
    auto connection = std::make_unique<TcpConnection>(endpoint);
    connection->send(order_message);
}  // Connection automatically closed here

// Geteilte Eigentümerschaft für referenzgezählte Ressourcen
auto config = std::make_shared<TradingConfig>(load_config());
engine.set_config(config);  // Multiple owners, automatic cleanup
Stack vs. Heap auf dem Hot Path Stack / Thread-lokal OrderUpdate auf dem Stack — begrenzt, LIFO, cache-heiß Pool / Arena — O(1)-Wiederverwendung, keine malloc-Last Heap (generisch) new/malloc — Allokator-Locks, Fragmentierung Unvorhersehbare Latenz — pro Tick vermeiden
Halte Structs auf dem Stack oder in vorgewärmten Pools; behandle Heap-Zugriffe im Tick-Handler wie Bugs.

Lock-freie Datenstrukturen und Nebenläufigkeit

Mutexe sind der Feind latenzarmen Codes. Ein einziger std::mutex::lock()-Aufruf kann selbst ohne Konkurrenz 20–100 Nanosekunden dauern — und unter Konkurrenz kann er einen Thread für Mikrosekunden blockieren. Trading-Systeme nutzen stattdessen lock-freie Datenstrukturen.

Die kritischste lock-freie Struktur im Trading ist die 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;
    }
};

Zentrale Designprinzipien:

  • alignas(64) — Jede atomare Variable erhält ihre eigene Cache-Line, was False Sharing zwischen CPU-Kernen verhindert.
  • Memory Orderingacquire/release-Semantik ist günstiger als seq_cst und für Producer-Consumer-Muster ausreichend.
  • Größen als Zweierpotenz — In der Produktion werden Größen wie 1024 oder 4096 verwendet, damit der Modulo zu einer bitweisen UND-Operation wird.

Typische Verdrahtung: Der NIC-Thread reiht ein, der Strategie-Thread nimmt heraus — kein Mutex auf dem Übertragungspfad, wenn die Topologie richtig gewählt ist.

SPSC-Ringpuffer (ein Schreiber, ein Leser) Producer Feed-/I/O-Thread Slots als Zweierpotenz • acquire/release-Atomics Consumer Strategie-Thread alignas(64) head/tail — getrennte Cache-Lines gegen False Sharing Memory Order: relaxed auf lokalem Index, acquire/release bei der Übergabe
Ein Producer und ein Consumer erlauben, auf Mutexe zu verzichten; korrekte Ausrichtung verhindert, dass Kerne um dieselbe Cache-Line konkurrieren.

Templates: Berechnung zur Compile-Zeit

C++-Templates erlauben es, Arbeit von der Laufzeit auf die Compile-Zeit zu verschieben. Im Trading bedeutet das, dass deine Binary exakt auf die Protokolle, Instrumente und Strategien spezialisiert ist, die du handelst — ohne Laufzeit-Verzweigungen auf dem 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);
    }
}

Die if constexpr-Zweige werden vollständig zur Compile-Zeit aufgelöst — der generierte Maschinencode enthält nur den relevanten Pfad, ohne Overhead durch Verzweigungen. Diese Technik erzeugt, kombiniert mit Link-Time-Optimierung (LTO), Binaries, bei denen das Protokoll-Parsing im Wesentlichen zu einer geraden Folge von Speicherzugriffen entrollt wird.

Moderne C++20/23-Features wie consteval, Concepts und Compile-Zeit-Container treiben das noch weiter, sodass ganze Order-Validierungs-Pipelines zur Compile-Zeit berechnet werden können.

Cache-freundliches Design und Kernel-Bypass-Networking

Bei Latenzen im Sub-Mikrosekunden-Bereich wird die CPU-Cache-Hierarchie zu deinem wichtigsten Optimierungsziel. Ein Cache-Miss auf den Hauptspeicher kostet ~100 Nanosekunden — eine Ewigkeit, wenn dein gesamtes Latenzbudget 500 ns beträgt.

// SCHLECHT: Array of Pointers (AoP) — cache-feindlich
std::vector<std::unique_ptr<Order>> orders;  // Each access = pointer chase + cache miss

// GUT: Struct of Arrays (SoA) — cache-freundlich
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

Beim Networking fügt der TCP/IP-Stack des Linux-Kernels 5–15 Mikrosekunden Latenz pro Paket hinzu. Trading-Firmen umgehen ihn vollständig:

  • DPDK (Data Plane Development Kit) — pollt die NIC direkt aus dem Userspace und umgeht den Kernel. Erreicht Paketverarbeitung im Sub-Mikrosekunden-Bereich.
  • Solarflare OpenOnload — Kernel-Bypass mit einer vertrauten Socket-API. Weit verbreitet im Aktien- und Futures-Handel.
  • FPGA-NICs — Xilinx Alveo und ähnliche Karten können Marktdaten in Hardware parsen und Orders erzeugen, wobei Latenzen im Nanosekundenbereich erreicht werden.

Sogar dezentrale Börsen profitieren von diesen Prinzipien. Hyperliquids L1-Chain — die Plattformen wie GaiaEx antreibt — wurde mit Blick auf hohen Durchsatz und latenzarmen Konsens entwickelt, und Market Maker, die sich damit verbinden, nutzen optimierte C++-Clients, um die Zeit zwischen dem Empfang eines Preisupdates und dem Einreichen einer Order zu minimieren.

Wie Matching-Engines von Börsen gebaut werden

Im Herzen jeder Börse sitzt die Matching-Engine — die Komponente, die Kauf- und Verkaufsorders zusammenführt. Sie ist die latenzsensitivste Software im gesamten Finanzwesen und fast immer in C++ geschrieben.

Eine vereinfachte Matching-Engine-Architektur:

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

Matching-Engines in Produktion optimieren weit über dieses Grundgerüst hinaus:

  • Price-Time-Priorität — Orders zum gleichen Preis werden in der Reihenfolge ihres Eintreffens ausgeführt, nachverfolgt mit Nanosekunden-Zeitstempeln.
  • Vorab zugewiesene Order-Pools — Keine Heap-Allokation während des Matchings. Orders werden aus festen Pools recycelt.
  • Lockloses Design — Das Orderbuch jedes Instruments läuft auf einem dedizierten Kern. Kein Cross-Book-Locking nötig.
  • Deterministisches Replay — Jede Order und jedes Match wird in persistentem Speicher protokolliert, für regulatorische Compliance und Disaster Recovery.

Systeme auf diesem Niveau zu bauen ist der Bereich, in dem C++ wirklich glänzt. Keine andere verbreitete Sprache gibt dir gleichzeitig Kontrolle über Speicherlayout, Thread-Scheduling, Cache-Verhalten und Netzwerk-I/O — die vier Säulen des Ultra-Low-Latency-Engineerings. Ob du die nächste Börse baust, dich als Market Maker mit einer verbindest oder die Ausführung bei einer Prop-Trading-Firma optimierst, C++ bleibt der unangefochtene König.