C++, Rust e trading ad alta frequenza: dove la latenza deterministica decide l'argomento
Introduzione
I dibattiti sul linguaggio di programmazione sono generalmente tollerati perché la maggior parte dei sistemi può permettersi un po' di teatro. Un servizio è un po' inefficiente, una coda diventa più ampia del dovuto, una politica di riprova fa qualcosa di moralmente discutibile e tutti continuano a muoversi perché il prodotto funziona ancora, le entrate arrivano ancora e il grafico della latenza è brutto in modo sopravvissuto.
Il trading ad alta frequenza è meno sentimentale. Non importa quale lingua abbia vinto su Internet questo trimestre. Gli importa se i dati di mercato diventano stato, lo stato diventa una decisione e la decisione diventa un ordine prima che la finestra si chiuda. In questo tipo di ambiente, le opinioni eleganti che non possono sopravvivere alla misurazione vengono rapinate rapidamente e di solito senza preavviso.
Ecco perché la questione di C++ e Rust in HFT è interessante. Non perché una lingua sia sacra e l'altra sia fraudolenta, ma perché HFT è uno dei rari ambiti che costringe l'intera argomentazione a tradursi in un comportamento effettivo del sistema. Il percorso caldo mantiene la sua forma sotto pressione oppure no. La latenza della coda rimane disciplinata oppure no. Replay o dice la verità oppure no. Lì l’architettura non è un test della personalità. È una fattura.
Questo è anche il motivo per cui la risposta non è "C++ per sempre" o "riscrivi tutto in Rust perché la sicurezza è una cosa positiva e la paura è un modello di business". La risposta più onesta è più ristretta e quindi più utile. C++ domina ancora i percorsi HFT più caldi perché il mondo circostante di strumenti, gestione dei feed, controllo della memoria, profilazione e pratiche adiacenti all'hardware rimane estremamente modellato su C++. Rust è veramente utile attorno a quel nucleo, e talvolta all'interno di parti di esso scelte con cura, ma non cancella il fatto fondamentale che il trading a bassa latenza punisce gli errori di astrazione più velocemente di quanto la maggior parte dei team riesca a rinominare l'iniziativa.
Quindi la conversazione giusta non riguarda l’identità. Riguarda i confini del sistema. Quali parti dello stack necessitano di un controllo brutale su memoria, layout, code, affinità e comportamento dei cavi? Quali parti traggono maggiori benefici da vincoli di correttezza più forti e da valori predefiniti più sicuri? Quali parti meritano un trattamento ibrido invece della purezza tribale? Queste domande sono molto meno affascinanti dei sermoni linguistici, ma sono anche le domande che sopravvivono al contatto con la produzione.
Perché HFT fa sembrare costosa la cattiva filosofia tecnica
HFT è insolitamente bravo a smascherare una bugia ingegneristica familiare: la bugia secondo cui il comportamento medio è sufficiente. In molti prodotti comuni, un sistema può rimanere rispettabile nascondendo il caos occasionale dietro il throughput, i nuovi tentativi o la pazienza dell'utente. In HFT, la latenza media è interessante, ma il comportamento della coda è spesso la parte che in realtà ti umilia. Un sistema che sembra veloce finché non scatta nel momento sbagliato non è un sistema veloce in alcun senso commercialmente significativo. È un trucco di fiducia con un benchmark allegato.
Ecco perché gli ingegneri di HFT diventano allergici alle astrazioni imprecise. Imparano che un'allocazione extra sul percorso caldo non è "solo un'allocazione". È una possibile fonte di jitter. Un salto in coda non è "solo un salto in coda". È un altro luogo in cui il tempo viene immagazzinato, la coordinazione si espande e la visibilità peggiora. Una struttura ostile alla cache non è solo un difetto estetico. È una tassa continua su ogni evento di mercato che attraversa il sistema. Moltiplicalo per il volume reale del feed e all'improvviso una scelta di design da una presentazione diventa una voce ricorrente nel budget per la delusione.
Rust entra in questa conversazione con forza legittima perché la sicurezza della memoria è importante, la correttezza della concorrenza è importante e il codice di sistema merita valori predefiniti migliori di "fai attenzione mentre giochi con i coltelli su una fossa". Quella parte è vera. Ma HFT non premia la verità isolatamente. Premia la verità combinata. La sicurezza conta, sì. Lo stesso vale per i gestori di feed maturi, i confini ABI stabili, gli strumenti di riproduzione, l'iterazione basata sui profili, la cultura matura dell'integrazione degli scambi e la capacità di ispezionare esattamente cosa sta facendo la macchina quando il mercato è scortese. C++ arriva ancora con più di quell'infrastruttura circostante nella maggior parte degli ambienti HFT.
Questo è uno dei motivi per cui gli acquirenti e i leader dell’ingegneria dovrebbero opporsi alle narrazioni sulla purezza. Un linguaggio può essere eccellente in una dimensione ristretta ed essere comunque l'impostazione predefinita sbagliata per la parte di uno stack più sensibile ai tempi se l'ecosistema circostante, gli strumenti e l'esperienza del team non supportano il percorso di consegna effettivo. HFT è il luogo in cui vanno le adorabili verità locali per apprendere che l'intero percorso conta ancora di più.
Lo stack non è una cosa, quindi la scelta della lingua non dovrebbe fingere il contrario
Uno degli errori più stupidi nel lavoro serio sui sistemi è parlare dello "stack HFT" come se fosse un unico organismo tecnico con un linguaggio preferito. Non lo è. Si tratta di un insieme di percorsi con pressioni e costi di fallimento molto diversi.
Il percorso di acquisizione dei dati di mercato ha un temperamento. Il percorso di aggiornamento del portafoglio ordini ne ha un altro. La logica della strategia può essere numericamente densa ma strutturalmente ristretta. I controlli del rischio sono spesso sensibili alla latenza ma anche alla correttezza in un modo noioso, adulto e giuridicamente consequenziale. Le infrastrutture di simulazione e riproduzione possono privilegiare il determinismo e l’introspezione rispetto alla pura vanità dei nanosecondi. Gli strumenti del piano di controllo, gli aiutanti per l'implementazione e le superfici operatore si preoccupano dell'affidabilità, della manutenibilità e dell'igiene dell'integrazione molto più di quanto si preoccupino di eliminare cinque microsecondi da un percorso che nessun cliente vedrà mai.
Questo è importante perché è spesso il punto in cui inizia una conversazione sensata tra C++ e Rust. C++ rimane più forte quando il percorso è brutalmente caldo, attento all'hardware, ricco di integrazione e già circondato da anni di pratica operativa nativa. Rust diventa più attraente quando il percorso è ancora importante, ma il valore economico di default più forti, proprietà più chiara e minore esposizione al rischio di memoria supera il costo dell’attrito dell’ecosistema.
In pratica, ciò porta spesso a risultati ibridi. I percorsi di gestione dei feed e gateway più importanti rimangono in C++. Strumenti di riproduzione, convalida della configurazione, alcuni aiutanti dal lato del rischio, utilità di normalizzazione dei messaggi, strumenti di controllo o componenti interni rivolti all'operatore possono essere eccellenti candidati Rust. Questa non è indecisione. È l’età adulta architettonica. Il sistema viene trattato come un insieme di confini reali piuttosto che come un fandom linguistico con un data center.
Dove C++ possiede ancora i percorsi più caldi
C++ mantiene il suo posto in HFT per ragioni meno mistiche di quanto talvolta gli estranei immaginano. Il primo motivo è il controllo della memoria e del layout. HFT i percorsi caldi si preoccupano di quali dati convivono, di come si comportano le strutture nella cache, di come la proprietà appare sotto carico e se il sistema può rimanere disciplinato dall'allocazione quando il mercato smette di essere educato. C++ offre ancora agli ingegneri un potere insolitamente diretto su tali scelte, e lo fa all'interno di un ecosistema che ha già trascorso decenni a imparare quali costi "piccoli" sono segretamente grandi.
Il secondo motivo è la densità degli utensili. C++ in HFT non significa solo un linguaggio. Significa compilatori, disinfettanti, grafici delle fiamme, C++, VTune, cablaggi di riproduzione, adattatori di scambio, folklore sulle code, esperienza nell'allocazione e un vasto corpus di storie di guerre sulle prestazioni accumulate sotto pressione finanziaria. Lì le squadre non partono da zero. Ereditano una profonda cultura operativa, e quella cultura è importante perché HFT premia l'iterazione misurata molto più della pulizia retorica.
La terza ragione è la gravità dell’integrazione. Gli scambi, i percorsi di rete nativi, gli strumenti di acquisizione dei pacchetti, l'ottimizzazione del kernel adiacente, l'infrastruttura FPGA adiacente e l'intero ecosistema a bassa latenza sono ancora molto a loro agio in un mondo C e C++. Rust può interagire con quel mondo, e talvolta in modo molto efficace, ma "può interagire con" non è la stessa cosa di "è il percorso di minor attrito attraverso l'intero sistema". Nel HFT serio, l'attrito non è un disagio emotivo. Si tratta di una possibile tassa di latenza, una tassa di debug e una tassa di consegna allo stesso tempo.
C'è anche una ragione più sottile che conta di più nell'era AI: C++ ha semplicemente più memoria operativa disponibile per questo lavoro. I sistemi di codifica AI, la ricerca del codice, gli esempi pubblici, gli snippet dei fornitori, il folklore degli ottimizzatori e le tracce di debug sono più densi intorno a C++ nei sistemi a bassa latenza che intorno a Rust. Ciò non rende C++ più nobile. Rende più facile per gli esseri umani e gli strumenti AI collaborare all'interno di brutte basi di codice reali il cui fascino è scaduto anni fa.
Dove Rust aiuta effettivamente invece di adempiere alla moralità
Rust aiuta di più quando risolve un problema reale piuttosto che agire come un accessorio di personalità per i diagrammi architettonici. In HFT, i casi d'uso più forti di Rust spesso appaiono attorno al nucleo caldo piuttosto che al centro assoluto di esso.
Rust è utile per i componenti in cui i difetti di correttezza sono costosi ma il budget di latenza non viene misurato al microscopio. I livelli di convalida dei messaggi, gli strumenti di configurazione e distribuzione, alcuni percorsi di normalizzazione del protocollo, i servizi di controllo, le utilità amministrative, gli analizzatori offline e gli strumenti degli operatori interni possono trarre vantaggio dalla preferenza del linguaggio verso l’esplicitezza. Il punto non è sembrare moderno. Il punto è ridurre la classe di errori stupidi, ripetitivi e strutturalmente evitabili che distolgono l’attenzione da lavori più importanti.
Rust può anche aiutare nella scelta accurata di componenti quasi caldi quando il team ha la giusta esperienza e il confine è onesto. Un parser a bassa latenza, una macchina a stati limitati o un pezzo di infrastruttura deterministica possono essere un solido candidato Rust se il team riesce a tenere sotto controllo la FFI e la storia dell'allocazione e se il carico dell'ecosistema circostante viene compreso in anticipo anziché scoperto alle 2:40 del mattino durante un'implementazione che nessuno voleva.
Ma è proprio qui che le squadre hanno bisogno di disciplina. Rust non ha valore quando viene lasciato cadere nel mezzo di uno stack di scambio nativo come rinnovamento basato sulla fede. È utile quando il confine è pulito, il percorso di misurazione è ovvio e il costo operativo dell'integrazione è inferiore al guadagno in termini di sicurezza o manutenibilità che crea. Altrimenti, il progetto diventa un bellissimo caso di studio su come dedicare molto tempo alla progettazione spostando lateralmente l’incertezza.
Il confine conta più del sermone
Un errore comune nelle discussioni su C++ e Rust è presupporre che l'uso di Rust rimuova automaticamente il pericolo. Non è così. Cambia il luogo in cui si trova il pericolo. In HFT, la questione dei confini è particolarmente importante perché i percorsi caldi raramente finiscono al confine della lingua. Terminano ai confini della rete, ai confini delle code, ai confini della pianificazione, ai confini di FFI e ai confini del layout dei dati.
Se un componente Rust deve passare in un adattatore di scambio C++, parlare con una coda nativa, passare i dati a un motore strategico con presupposti di layout rigorosi o mantenere un comportamento deterministico attraverso le transizioni dei confini, allora il vero lavoro di ingegneria non è "abbiamo usato Rust". Il vero lavoro è con quanta cura è stata definita e verificata la cucitura. Comportamenti non sicuri possono ancora verificarsi a causa di mancata corrispondenza di ABI, confusione sulla proprietà, copie nascoste, errori di coda o sorprese temporali. La lingua da sola non è il tuo modello di governance. Il confine è.
Questo è il motivo per cui i team maturi parlano di un percorso stretto e caldo e di una superficie stretta e poco sicura. Non si basano su slogan come "sicurezza della memoria per impostazione predefinita" per risolvere quello che è fondamentalmente un problema di progettazione del sistema. Le buone squadre fanno domande più brutte e quindi più utili. Dove avviene la copia? Dov'è il salto in coda? Quale parte possiede il buffer? Quale percorso assegna? Cosa succede durante la contropressione? Cos'è rigiocabile? Cosa può essere valutato isolatamente e cosa deve essere valutato end-to-end perché le vittorie locali hanno una lunga tradizione di diventare delusioni globali?
Casi pratici che vale la pena risolvere prima
Il primo progetto più intelligente raramente è "riscrivere il percorso caldo". Questo è l'equivalente tecnico di entrare in una casa e decidere che il primo atto utile è sostituire l'intero scheletro prima di verificare quale tubo sta già allagando la cucina.
Il primo progetto migliore è uno di questi:
Lavoro di prova degli addetti alla lavorazione del mangime
Se il team discute se l'analisi, la normalizzazione, l'accodamento o il trasferimento siano davvero il problema della latenza, costruire prima il percorso delle prove. Cattura il traffico rappresentativo, riproducilo in modo deterministico e forza il sistema a confessare dove il tempo e il jitter stanno effettivamente entrando nella catena. La maggior parte dei sistemi HFT non hanno bisogno di più ideologia qui. Hanno bisogno di una macchina della verità migliore.
Gateway e pulizia dei confini del rischio
Molti stack non vengono rovinati dalla logica strategica principale. Sono rovinati dalla trascuratezza dei confini tra rischio, logica di accesso e coordinamento operativo. Un’attenta riscrittura o ristrutturazione di questi punti può migliorare l’affidabilità e la diagnosticabilità senza il rischio commerciale di toccare prima il circuito assolutamente più caldo.
Pulizia ibrida del piano di controllo
Se gli strumenti dell'operatore, gli aiutanti per la distribuzione, le utilità di ripristino o gli strumenti di riproduzione sono fragili, Rust può essere un buon candidato in questo caso. Questi componenti spesso determinano la salute dell’intera organizzazione anche quando non si trovano nel percorso più veloce del microsecondo. Strumenti più puliti possono rendere il sistema caldo più calmo senza pretendere che ogni binario nella tenuta meriti lo stesso linguaggio.
Laboratorio pratico: costruisci un piccolo rilevatore di gap di sequenza e rendilo onesto
Manteniamo il laboratorio piccolo e utile. I sistemi HFT vivono e muoiono grazie alla disciplina della sequenza molto prima di raggiungere un'affascinante logica strategica. Questo programma giocattolo riproduce un flusso simile a un feed e segnala dove sono apparse le lacune.
main.cpp
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>
struct Packet {
std::uint64_t seq;
std::string payload;
};
struct Gap {
std::uint64_t expected;
std::uint64_t received;
};
class GapDetector {
public:
void on_packet(const Packet& packet) {
if (!started_) {
expected_ = packet.seq + 1;
started_ = true;
return;
}
if (packet.seq != expected_) {
gaps_.push_back({expected_, packet.seq});
}
expected_ = packet.seq + 1;
}
const std::vector<Gap>& gaps() const {
return gaps_;
}
private:
bool started_ = false;
std::uint64_t expected_ = 0;
std::vector<Gap> gaps_;
};
int main() {
std::vector<Packet> replay{
{1001, "AAPL bid"},
{1002, "AAPL ask"},
{1003, "MSFT bid"},
{1007, "MSFT ask"},
{1008, "NVDA bid"},
{1011, "NVDA ask"}
};
GapDetector detector;
for (const auto& packet : replay) {
detector.on_packet(packet);
}
if (detector.gaps().empty()) {
std::cout << "no gaps\n";
return 0;
}
for (const auto& gap : detector.gaps()) {
std::cout << "gap expected=" << gap.expected
<< " received=" << gap.received << "\n";
}
}
Costruire
Su Linux o macOS:
g++ -O2 -std=c++20 -o gap_detector main.cpp
./gap_detector
Su Windows:
cl /O2 /std:c++20 main.cpp
.\main.exe
Perché questo piccolo esercizio è importante
Perché forza il giusto tipo di pensiero:
- aggiornamento deterministico dello stato
- sequenza onesta
- riproduzione prima della teoria
- comportamento limitato e misurabile
Questo è già più HFT di un numero sorprendente di diapositive di conferenze.
Attività di prova per appassionati
- Trasferisci lo stesso rilevatore su Rust e confronta non la vanità del benchmark, ma la chiarezza dei confini, l'attrito delle dipendenze e la facilità con cui ciascuna versione si adatta ai tuoi strumenti esistenti.
- Estendi la riproduzione in modo che i pacchetti mancanti possano successivamente arrivare fuori ordine, quindi decidi se il rilevatore deve memorizzarli nel buffer, rifiutarli o contrassegnarli.
- Aggiungi temporizzazione e misura la differenza tra una riproduzione supportata da vettori e una riproduzione supportata da buffer ad anello.
- Introduci un’allocazione non necessaria sul percorso caldo e misura quanto velocemente una decisione “piccola” inizia a contaminare il risultato.
- Aggiungi un ramo di registrazione all'interno di
on_packete osserva quanto velocemente l'osservabilità diventa un sabotaggio quando viene posizionata con noncuranza.
Riepilogo
La vera conversazione tra C++ e Rust in HFT non riguarda quale linguaggio meriti la mitologia più bella. Riguarda quali parti del sistema necessitano di un controllo diretto, quali beneficiano di impostazioni predefinite più forti e quali confini possono essere resi sufficientemente onesti da supportare la progettazione ibrida senza illusioni.
C++ domina ancora i percorsi HFT più interessanti perché il dominio premia il controllo sul layout della memoria, sull'accodamento, sul comportamento dei cavi, sulla profilazione, sulla riproduzione e sull'integrazione con un ecosistema maturo a bassa latenza. Rust è utile laddove la correttezza, l'esplicitezza e la manutenibilità creano più valore rispetto ai costi aggiuntivi di attrito dell'ecosistema. Entrambi possono appartenere a uno stack serio. La mossa degli adulti è decidere dove e lasciare che siano le prove piuttosto che il fandom del linguaggio a tenere il punteggio.
Riferimenti
- Specifiche NASDAQ TotalView-ITCH: ITCH
- FIX Standard della comunità commerciale: FIX
- Documentazione DPDK: https://doc.dpdk.org/guides/
- Linux documentazione di timestamp: Linux
- Brendan Gregg sui grafici delle fiamme: https://www.brendangregg.com/flamegraphs.html
- Il Rust Libro delle prestazioni: Rust