
C++ für Low-Latency-Trading-Systeme
Warum die schnellsten Trading-Firmen alles in C++ schreiben
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.
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 cleanupLock-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 Ordering —
acquire/release-Semantik ist günstiger alsseq_cstund 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.
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.