← Alle Insights

C++ Low-Latency Orderbook: Architektur und Messung.

Ein Orderbook ist das Herz jeder Matching-Engine und jeder ernsthaften Market-Making- Strategie. In Low-Latency-Umgebungen entscheidet die Qualität dieses einen Datenobjekts über Mikrosekunden — und damit über Edge oder kein Edge. Ein nüchterner Blick auf Architektur, Allokation und Messung.

Was ein Orderbook leisten muss.

Vereinfacht gesagt verwaltet ein Orderbook zwei sortierte Listen: Bids absteigend, Asks aufsteigend. Jede Seite besteht aus Preisleveln, jedes Preislevel aus einer FIFO-Liste individueller Orders. Vier Operationen müssen schnell sein: Insert einer neuen Order, Cancel einer existierenden Order, Modify (oft als Cancel-Replace umgesetzt) und Top-of-Book-Abfrage (bestes Bid, bestes Ask, jeweilige Volumina).

Die Anforderungen unterscheiden sich je nach Anwendung: Eine Matching-Engine sieht primär kleine, gut verteilte Updates. Ein Market-Data-Feed muss Bursts von zehntausenden Updates pro Sekunde verkraften. Eine Strategie-seitige Sicht braucht nur die obersten zehn Level — andere Designs als bei einer Vollabbildung.

Datenstrukturen — was funktioniert.

Die naive Wahl ist eine Map (std::map oder std::unordered_map), indiziert auf den Preis. Funktioniert, ist aber wegen Pointer-Chasing und Heap-Allokation in der Praxis langsam. Bessere Designs nutzen die Tatsache, dass Preise in einem festen Tick-Raster vorliegen und ein begrenztes Fenster aktiv ist.

Drei bewährte Ansätze:

Ein minimaler Entwurf.

Ein vereinfachter Bid/Ask-Container mit Custom-Allokator und intrusivem FIFO pro Level:

#include <array>
#include <cstdint>
#include <cstddef>

struct Order {
    std::uint64_t id;
    std::uint32_t qty;
    Order* prev{nullptr};
    Order* next{nullptr};
};

struct Level {
    Order* head{nullptr};
    Order* tail{nullptr};
    std::uint64_t volume{0};

    void push_back(Order* o) noexcept {
        o->prev = tail; o->next = nullptr;
        if (tail) tail->next = o; else head = o;
        tail = o;
        volume += o->qty;
    }

    void erase(Order* o) noexcept {
        if (o->prev) o->prev->next = o->next; else head = o->next;
        if (o->next) o->next->prev = o->prev; else tail = o->prev;
        volume -= o->qty;
    }
};

template <std::size_t N>
class BookSide {
public:
    void insert(std::uint32_t price_idx, Order* o) noexcept {
        levels_[price_idx].push_back(o);
        if (price_idx > best_) best_ = price_idx;
    }

    void cancel(std::uint32_t price_idx, Order* o) noexcept {
        levels_[price_idx].erase(o);
        while (best_ > 0 && levels_[best_].head == nullptr) --best_;
    }

    const Level& top() const noexcept { return levels_[best_]; }

private:
    std::array<Level, N> levels_{};
    std::uint32_t best_{0};
};

Das Beispiel ist absichtlich knapp und ignoriert echte Anforderungen wie Cross-Detection, Locking und ID-Lookup. Es zeigt das Grundmuster: vorab allokierter Levelspeicher, intrusiv verkettete Orders, kein dynamischer Speicher im Hot Path.

Cache, Cache, Cache.

Die mit Abstand wichtigste Optimierung in modernem Numerik- und Trading-Code ist nicht der Algorithmus, sondern das Verhalten gegenüber dem Cache. Ein L1-Hit kostet rund 1 ns, ein L3-Hit ungefähr 10 ns, ein DRAM-Zugriff schon 100 ns. Wer in einem Burst zehntausend Orders verarbeiten will, kann nicht für jede Operation einen Cache-Miss zahlen.

Praktische Folgen: Level-Strukturen so klein halten, dass mehrere in eine Cache-Line passen (typisch 64 Byte). Order-Pools kontinuierlich allokieren statt mit new. Hot- und Cold-Daten trennen — Statistiken, Audit-IDs, Strings gehören nicht in dieselbe Struktur wie die Verkettung. Branch- Vorhersage durch [[likely]]/[[unlikely]] oder Daten-getriebene Sortierung helfen, Pipeline-Stalls zu vermeiden.

Allokation: warum kein malloc im Hot Path.

Ein Aufruf von malloc kann je nach Implementierung zwischen 30 ns und mehreren Mikrosekunden dauern. Die Varianz ist das eigentliche Problem: Sie zerstört Latenz-Percentiles. Die übliche Antwort ist ein Object-Pool, der zur Initialisierung einmal eine große Region allokiert und Objekte daraus liefert:

template <typename T, std::size_t N>
class FixedPool {
public:
    FixedPool() noexcept {
        for (std::size_t i = 0; i < N - 1; ++i)
            slots_[i].next = &slots_[i + 1];
        slots_[N - 1].next = nullptr;
        free_ = &slots_[0];
    }

    T* acquire() noexcept {
        if (!free_) return nullptr;
        auto* s = free_;
        free_ = s->next;
        return reinterpret_cast<T*>(&s->storage);
    }

    void release(T* p) noexcept {
        auto* s = reinterpret_cast<Slot*>(p);
        s->next = free_;
        free_ = s;
    }

private:
    union Slot {
        Slot* next;
        alignas(T) unsigned char storage[sizeof(T)];
    };
    std::array<Slot, N> slots_{};
    Slot* free_{nullptr};
};

Ein Acquire/Release kostet so etwa 5 ns, deterministisch. Die Allokation ist damit faktisch aus dem kritischen Pfad verschwunden.

Threading und Concurrency.

Die Konsens-Architektur für Low-Latency-Orderbooks ist Single-Writer: genau ein Thread modifiziert das Book, alle anderen lesen über lock-freie Snapshots oder Ringbuffer (LMAX Disruptor lässt grüßen). Locks im Hot Path sind ein No-Go: ein Mutex-Contention-Ereignis bedeutet im schlimmsten Fall einen Kontextwechsel, also mehrere Mikrosekunden Verlust.

Wer Multi-Producer braucht, nutzt MPSC-Queues und serialisiert in einem Engine-Thread. Klingt nach Engpass, ist in der Praxis fast immer schneller als Locking, weil keine teuren Atomics auf dem kritischen Pfad nötig sind.

Messen, nicht raten.

Jede Optimierung muss messbar sein. Sinnvolle Werkzeuge: perf stat für Cache-Misses und Branch-Mispredictions, perf record/perf report für Hotspot-Profiling, vtune für detaillierte Microarchitektur-Analyse, eigene rdtsc-basierte Timer mit Histogrammen für End-to-End-Latenz. Verlassen Sie sich nie auf Mittelwerte allein — Percentile (P50, P95, P99, P99.9) sind das, was in produktiven Systemen zählt.

Wann das alles übertrieben ist.

Nicht jedes Trading-System braucht ein 1-Mikrosekunden-Orderbook. Wer Strategien mit Haltedauern von Sekunden oder länger handelt, verliert keine Edge an einem Python- Orderbook. Diese Mühe lohnt sich primär für Market-Making, Latency-Arbitrage und ähnliche Anwendungen, in denen Mikrosekunden direkt in Fill-Quoten übersetzbar sind.

Sie bauen ein eigenes Orderbook oder optimieren ein bestehendes? Erstgespräch buchen — wir prüfen die Architektur und identifizieren die echten Hotspots.