Oprócz doskonałej odpowiedzi dotyczącej strojenia sprzętu / konfiguracji z @jimwise, „linux o niskim opóźnieniu” oznacza:
- C ++ ze względu na determinizm (brak opóźnienia zaskoczenia podczas uruchamiania GC), dostęp do urządzeń niskiego poziomu (I / O, sygnały), mocy językowej (pełne wykorzystanie TMP i STL, bezpieczeństwo typu).
- wolą szybkość nad pamięcią: częste jest> 512 Gb pamięci RAM; bazy danych są w pamięci, buforowane z góry lub egzotyczne produkty NoSQL.
- wybór algorytmu: tak szybko, jak to możliwe w porównaniu z rozsądnym / zrozumiałym / rozszerzalnym, np. tablice wielobitowe bez blokady, zamiast właściwości obiektów z funkcją bool.
- pełne wykorzystanie funkcji systemu operacyjnego, takich jak pamięć współdzielona między procesami na różnych rdzeniach.
- bezpieczne. Oprogramowanie HFT jest zwykle zlokalizowane na giełdzie, więc możliwości złośliwego oprogramowania są niedopuszczalne.
Wiele z tych technik pokrywa się z tworzeniem gier, co jest jednym z powodów, dla których branża oprogramowania finansowego absorbuje ostatnio zwolnionych programistów gier (przynajmniej dopóki nie spłacą zaległych czynszów).
Podstawową potrzebą jest możliwość słuchania bardzo szerokiego pasma danych rynkowych, takich jak ceny papierów wartościowych (akcje, towary, kursy walutowe), a następnie podejmowanie bardzo szybkich decyzji kupna / sprzedaży / braku działania w oparciu o bezpieczeństwo, cenę i bieżące gospodarstwa.
Oczywiście to wszystko może również pójść spektakularnie źle .
Omówię więc punkt dotyczący tablic bitów . Załóżmy, że mamy system transakcyjny wysokiej częstotliwości, który działa na długiej liście zamówień (Kup 5 tys. IBM, Sprzedaj 10 tys. DELL itp.). Powiedzmy, że musimy szybko ustalić, czy wszystkie zamówienia są wypełnione, abyśmy mogli przejść do następnego zadania. W tradycyjnym programowaniu OO będzie to wyglądać następująco:
class Order {
bool _isFilled;
...
public:
inline bool isFilled() const { return _isFilled; }
};
std::vector<Order> orders;
bool needToFillMore = std::any_of(orders.begin(), orders.end(),
[](const Order & o) { return !o.isFilled(); } );
złożoność algorytmiczna tego kodu będzie równa O (N), ponieważ jest to skan liniowy. Spójrzmy na profil wydajności pod względem dostępu do pamięci: każda iteracja pętli wewnątrz std :: any_of () wywoła o.isFilled (), która jest wstawiona, więc staje się dostępem do pamięci _isFilled, 1 bajt (lub 4 w zależności od architektury, kompilatora i ustawień kompilatora) w obiekcie, powiedzmy 128 bajtów łącznie. Mamy więc dostęp do 1 bajtu na każde 128 bajtów. Kiedy czytamy 1 bajt, zakładając, że jest to najgorszy przypadek, otrzymamy brak pamięci podręcznej danych procesora. Spowoduje to żądanie odczytu do pamięci RAM, które odczytuje całą linię z pamięci RAM ( więcej informacji tutaj ), aby odczytać 8 bitów. Profil dostępu do pamięci jest więc proporcjonalny do N.
Porównaj to z:
const size_t ELEMS = MAX_ORDERS / sizeof (int);
unsigned int ordersFilled[ELEMS];
bool needToFillMore = std::any_of(ordersFilled, &ordersFilled[ELEMS+1],
[](int packedFilledOrders) { return !(packedOrders == 0xFFFFFFFF); }
profil dostępu do pamięci, przy założeniu, że jest to najgorszy przypadek, to ELEMS podzielony przez szerokość linii RAM (różni się - może być dwukanałowy lub potrójny, itp.).
W efekcie optymalizujemy algorytmy pod kątem wzorców dostępu do pamięci. Żadna ilość pamięci RAM nie pomoże - to wielkość pamięci podręcznej procesora powoduje tę potrzebę.
czy to pomaga?
Na YouTube jest doskonała rozmowa o CPPCon na temat programowania z niskim opóźnieniem (dla HFT): https://www.youtube.com/watch?v=NH1Tta7purM