domain: Regeln für niedrige Latenz (low-latency rules)
[R:eine-messung-stimmt-mit-dem-berichteten-regime-ueberein] Eine Messung gibt an, ob sie warm oder kalt lief, und entspricht der Produktion [R:a-measurement-matches-the-regime-it-reports]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus JMH und seinen Beispielen (Aleksey Shipilev), deren Existenz die stehende Behauptung ist, dass ein handgemachter Mikrobenchmark auf der JVM standardmäßig falsch ist.
Geben Sie an, in welchem Regime die Messung durchgeführt wurde, und prüfen Sie, ob es dem Regime der Produktion entspricht. Hier leben zwei Fehler, und nur einer von ihnen hat einen Ruf.
Messen von kalt, wenn die Produktion warm ist. Eine erste Ausführung läuft interpretiert oder aus einem kalten Instruktionscache, mit einem untrainierten Sprungvorhersager, ungefüllten Seitentabellen und, bei einer verwalteten Laufzeit, überhaupt ohne kompilierten Code. Der Wert kann ein bis zwei Größenordnungen schlechter sein als der stationäre Zustand und beschreibt eher das Aufwärmen als die eigentliche Arbeit. Auf der JVM erfolgt die Eskalation stufenweise – Interpreter, dann C1, dann C2, mit On-Stack-Replacement für langlebige Schleifen – und benötigt Tausende von Iterationen, weshalb eine handgemachte Zeitschleife standardmäßig falsch ist und ein Framework existiert.
Messen von warm, wenn die Produktion kalt ist, was die Hälfte ist, die keine Beachtung findet, weil sie nach Strenge aussieht. Ein Pfad, der einmal pro Minute läuft, ist niemals warm. Ein Börseneröffnungs-Burst trifft auf kalten Code. Ein Fehlerhandler läuft zum ersten Mal an diesem Tag im ungünstigsten Moment. Für jeden dieser Fälle ist der warme Wert eine Zahl, die kein Benutzer jemals sehen wird, und das Aufwärmen, das sorgfältig verworfen wurde, ist das Produktionsverhalten. Es zu verwerfen ist dort keine Hygiene; es ist das Löschen der Messung.
Die Regel ist also eine Bereichsübereinstimmung und kein Verfahren: Sagen Sie, welches Regime gemessen wurde, sagen Sie, welches Regime die Produktion hat, und zeigen Sie, dass sie identisch sind. Wo die Produktion tatsächlich beides ist – ein gleichmäßiger Strom mit kalten Ausbrüchen – sind das zwei Messungen und zwei berichtete Zahlen, nicht eine, die ins Bedeutungsleere gemittelt wurde.
Dies ist [R:messen-sie-die-behauptung-keine-Teilmenge], angewendet auf die Zeit statt auf eine Menge: Die Aufwärmsamples sind eine Teilmenge, das Verwerfen ist eine Bereichsentscheidung, und eine Bereichsentscheidung aus Reflex ist diejenige, die eine falsche Zahl mit der Autorität des Gemessenseins herstellt.
Fehlermodus-Prüfung: Ist der Produktionspfad warm oder kalt, wenn es darauf ankommt, und was habe ich gemessen? Wenn der Benchmark die ersten N Iterationen verworfen hat, nennen Sie, was die Produktion hat, das diesen Iterationen entspricht. Wenn nichts darauf zutrifft, war der verworfene Teil die Antwort.
Zwei Nachbarn, bewusst getrennt. [R:kein-koordiniertes-Auslassen] ist das Instrument, das während der relevanten Ereignisse keine Samples nimmt; dies ist das Instrument, das sie im falschen Regime nimmt. Und [R:verifizieren-dass-die-Abstraktion-wegoptimiert-wurde] hält den Fall, in dem der Optimierer die Arbeit entfernt hat, die der Benchmark zeitlich erfasst hat – eine warme Messung von gar nichts.
[R:eine-queue-ohne-grenze-hat-kein-ueberlastungsverhalten] Eine unbegrenzte Warteschlange wandelt Überlastung in Latenz um, anstatt in Verweigerung [R:a-queue-without-a-bound-has-no-overload-behaviour]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus Little's Law (J. D. C. Little, 1961): L = lambda W, also für eine Warteschlange, die niemals verweigert, wächst die Wartezeit unbegrenzt, sobald die Ankunftsrate die Bearbeitungsrate übersteigt.
Entscheiden Sie, was die Warteschlange tut, wenn der Verbrauch hinter der Produktion zurückfällt. Wachsen ist keine Entscheidung.
Warteschlangentiefe ist Latenz – das ist Little's Law und keine Heuristik: Bei Ankünften mit Rate lambda und mittlerer Warteschlangenlänge L beträgt die mittlere Wartezeit L/lambda, also ist eine unbegrenzte L eine unbegrenzte Wartezeit. Eine Warteschlange ohne Kapazitätsgrenze hat daher keine Latenzgrenze, und eine Latenzanforderung, die irgendwo vorgelagert angegeben wurde, wird von nichts eingehalten.
Der Fehler hat eine charakteristische Form, die ihn spät diagnostizieren lässt. Produzenten blockieren niemals, also meldet nichts Vorgelagertes ein Problem; der Durchsatz sieht korrekt aus, da jedes Element irgendwann verarbeitet wird; und das einzige Symptom ist, dass die Latenz monoton ansteigt. Wenn es sichtbar wird, erledigt der Rückstand Arbeit, die niemand will – jedes verbrauchte Element ist bereits veraltet, also wird Kapazität dafür aufgewendet, Antworten auf Fragen zu liefern, deren Zeitpunkt vorbei ist, weshalb sich das System nicht erholt, wenn die Last wieder normal wird. Es muss sich zuerst leeren, mit derselben Rate, die zurückgefallen war.
Begrenzen Sie also die Warteschlange und wählen Sie das Verhalten an der Grenze. Vier sind legitim, und die Wahl ist eine Domänenentscheidung, keine technische:
- Blockieren des Produzenten. Echter Gegendruck – der langsamste Verbraucher bestimmt die Rate für alle, und der Druck pflanzt sich fort zu dem, der ihn tatsächlich ablegen kann. Falsch, wenn der Produzent nicht gestoppt werden darf, was das Regime ist, um das es bei
[R:kein-stau-innerhalb-eines-veroeffentlichungsfensters]geht. - Ältestes verwerfen. Richtig für Marktdaten, Sensorwerte, Telemetrie: Ein veralteter Tick hat negativen Wert, und der neueste ist der einzige, den es zu behalten lohnt.
- Neuestes verwerfen. Richtig, wenn der Rückstand eine faire Warteschlange ist und Nachzüglern nichts versprochen wurde.
- Ablehnen und dies mitteilen. Richtig für Order-Eingabe und alles, bei dem ein Aufrufer das Schicksal kennen muss. Eine stillschweigend verworfene Order ist schlechter als eine abgelehnte, um die gesamte Breite des Vertrauens, das der Aufrufer in den Aufruf gesetzt hat.
Nicht zu wählen, wählt ein fünftes Verhalten, das niemand bewusst wählen würde: Alles absorbieren, die Latenz jedes Elements gleichermaßen verschlechtern und schließlich durch Erschöpfung des Speichers scheitern – oder, bei einer verwalteten Laufzeit, durch eine durch den Rückstand selbst verursachte Sammlungspause, die den Verbraucher weiter verlangsamt und das ist das Nächste an einer Rückkopplungsschleife mit dem falschen Vorzeichen in der Produktion.
Die unbegrenzte Struktur bleibt die richtige Wahl, wo die Grenze tatsächlich woanders liegt – eine Warteschlange, deren Produzenten vorgelagert ratenbegrenzt sind, oder deren Verbraucher nachweislich schneller ist als jede mögliche Ankunftsrate. Sagen Sie, welche, im Code, neben der Warteschlange. Eine unbegrenzte Warteschlange mit einem genannten Grund ist eine Entscheidung; eine ohne ist das Fehlen einer Entscheidung, die dieselbe Form trägt.
Fehlermodus-Prüfung: Wenn der Verbrauch für zehn Sekunden hinter der Produktion zurückfällt, was tut diese Warteschlange und wer findet es heraus? Wenn die Antwort „sie wächst“ und „niemand“ ist, gibt es hier kein Überlastungsverhalten, sondern nur ein auf sein Auftreten wartendes Überlastungssymptom.
[R:eine-struktur-behaelt-das-regime-unter-dem-sie-bewiesen-wurde] Eine Struktur ist nur unter dem Nebenläufigkeitsregime korrekt, das ihr Beweis annahm [R:a-structure-keeps-the-regime-it-was-proved-under]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben von Dmitry Vyukov, Intrusive MPSC Node-basierte Queue, 1024cores – wo das Regime im Namen des Algorithmus und nirgendwo in seinem Code angegeben ist.
Lesen Sie das Regime aus dem Korrektheitsargument der Struktur heraus, und nennen Sie dann, was im Code dessen Verletzung verhindert. „Die Dokumentation sagt Single-Consumer“ ist kein Mechanismus.
MPSC bedeutet Multi-Producer, Single-Consumer, und das Single ist eine Vorbedingung und keine Beschreibung. Vyukovs mpscq_pop liest self->tail, geht von dort aus und schreibt es zurück – ein einfacher Lade- und ein einfacher Speichervorgang, unsynchronisiert, weil genau ein Thread als Ausführer angenommen wurde. Zwei Verbraucher, die sich dort verschränken, können beide denselben Knoten zurückgeben, oder einer kann einen Knoten ganz verlieren. Keines der Ergebnisse löst etwas aus. Es gibt keine Behauptung, keinen zu konkurrierenden Lock, keinen Fehlerpfad: Die Warteschlange gibt plausible Werte zurück, und das Programm fährt mit einem duplizierten oder verschwundenen Element fort.
Diese Stille ist das ganze Problem, und sie wird durch Last verstärkt. Zwei Verbraucher, die hundert Operationen ausführen, können sich möglicherweise nie in dem relevanten Zeitfenster verschränken; die Testsuite besteht, der Staging-Soak besteht, und die Verschränkung tritt mit Produktionstraffic auf. Ein Fehler, der nur unter Bedingungen auftritt, die Sie nicht reproduzieren können, ist von außen nicht von einem Fehler ganz woanders zu unterscheiden.
Setzen Sie also das Regime in den Typ, wo die Sprache es erlaubt. Geben Sie bei der Konstruktion genau ein Verbraucher-Handle aus; machen Sie es weder Clone noch Sync; nehmen Sie &mut self bei pop, sodass der Borrow-Checker die Exklusivität durchsetzt. Dann ist „zwei Verbraucher“ kein zu findender Fehler, sondern ein Programm, das nicht kompiliert – [R:mache-illegale-zustaende-undarstellbar], und [R:typzustand-fuer-protokolle], wo das Regime sich über die Lebensdauer einer Struktur ändert. Dies ist die höchste Hebelwirkung dieser Disziplin in der gesamten Domäne, weil die Alternative ein Satz in einem Header-Kommentar ist und der Fehler, den sie verhindert, nicht zuschreibbar ist.
Wo die Sprache es nicht trägt – Java hat kein solches Handle – muss die Kontrolle etwas sein, das tatsächlich läuft: den besitzenden Thread bei der ersten Verwendung aufzeichnen und bei jedem nachfolgenden Aufruf in einem Debug-Build behaupten, oder den gesamten Verbrauch durch ein einzelnes Objekt leiten, dessen Konstruktion der einzige Ort ist, an dem die Invariante angegeben wird. Ein Kommentar ist keine Kontrolle ([R:garantie-braucht-einen-leser]).
Die Klasse ist breiter als Warteschlangen, und die anderen Mitglieder versagen auf dieselbe Weise. Ein SPSC-Ring mit zwei Produzenten. Eine Struktur, die als nicht wiedereintrittsfähig dokumentiert ist, die aber von einem Signal-Handler oder Callback betreten wird. Eine HashMap, die von zwei Threads erreicht wird, weil das Feld, das sie hielt, drei Refactorings später gemeinsam genutzt wurde. Ein Iterator, der über eine Mutation hinweg gehalten wird. In jedem Fall war das Regime eine Hypothese des Korrektheitsarguments und nichts im Artefakt zeichnet es auf.
Fehlermodus-Prüfung: Welches Regime nimmt das Korrektheitsargument dieser Struktur an, und was würde fehlschlagen, wenn ich es jetzt verletzen würde? Wenn die ehrliche Antwort „der Name sagt es“ oder „wir sind vorsichtig“ ist, ist die Struktur ungeschützt, und die nächste Person, die danach greift, wird es nicht wissen.
[R:alloziert-ist-nicht-resident] Speicher, der nie berührt wurde, ist noch kein Speicher [R:allocated-is-not-resident]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben von Ulrich Drepper, "What Every Programmer Should Know About Memory" (2007), über Demand Paging und die Kosten einer ersten Berührung.
Vorzuweisen ist nicht vorbereiten. Eine erfolgreiche Allokation kauft Adressraum; die Seite kommt bei der ersten Berührung, und die Berührung ist das, was kostet.
malloc und mmap geben virtuelle Adressen zurück. Physische Seiten werden verzögert bereitgestellt: Der erste Schreibzugriff auf jede Seite fängt im Kernel, der einen Rahmen findet, ihn nullt – weil die Daten des vorherigen Mieters nicht durchsickern dürfen – und ihn zuordnet. Ein Ein-Gigabyte-Puffer, der beim Start allokiert und zum ersten Mal auf dem heißen Pfad geschrieben wird, enthält also etwa eine Viertelmillion Seitenfehler, die pro Seite auftreten, verteilt über den ersten Durchlauf. Jede Allokations-freie Behauptung auf diesem Pfad ist erfüllt, und jede von ihnen misst das Falsche: Nichts hat den Allokator aufgerufen.
Residenz ist auch nicht dauerhaft, sobald sie erreicht ist. Seiten können bei Speicherdruck zurückgefordert oder ausgelagert werden, also kann ein langlebiger Prozess, der seine Puffer beim Start berührt und dann untätig gelassen hat, wieder auf einem Pfad fehlschlagen, der eine Woche lang fehlerfrei war. Deshalb hat das Gegenmittel zwei Hälften und deshalb ist es ein häufiger und teurer Fehler, nur die erste zu tun.
Also, für alles auf einem Pfad mit einer Frist: Schreiben Sie auf jede Seite, die der Pfad berühren wird, beim Start, bevor echte Arbeit eintrifft – die Berührung muss ein Schreiben sein, da ein Lesen einer unberührten Seite von der gemeinsamen Nullseite bedient werden kann und nichts beweist. Dann pinnen Sie sie: mlockall(MCL_CURRENT | MCL_FUTURE), und kein Swap auf der Box. Auf der JVM macht -XX:+AlwaysPreTouch die erste Hälfte für den Heap und ist den langsameren Start genau deshalb wert, weil die Kosten von dem Pfad mit einer Grenze wegverlagert werden.
Huge Pages ändern die Arithmetik in beide Richtungen und verdienen ihre eigene Entscheidung statt einer Voreinstellung. Weniger, größere Seiten bedeutet weniger Fehler und weniger TLB-Druck; jeder Fehler ist größer, und transparente Huge Pages bringen einen Defragmentierungspfad mit sich, der einen Thread für Millisekunden im genau falschen Moment anhalten kann. Explizite Huge Pages, die beim Boot reserviert werden, vermeiden das; THP auf always zu lassen und zu hoffen, ist die Form, die diese Domäne immer wieder benennen muss.
Berühren Sie von dem Thread, der den Speicher besitzen wird, nicht von dem Thread, der das Setup durchführt. Auf einer Multi-Socket-Maschine entscheidet der erste Schreibzugriff auch über die Platzierung: Eine Seite wird auf dem Knoten desjenigen allokiert, der sie fehlerhaft macht, also legt die naheliegende Art, diese Regel zu erfüllen – eine Schleife im Initialisierungsthread, die jeden Puffer durchläuft – jede Seite auf dem Knoten dieses Threads, und ein Arbeiter, der auf den anderen Socket gepinnt ist, liest dann alles über die Verbindung mit etwa der doppelten lokalen Latenz. Der Pfad ist jetzt fehlerfrei und vollständig entfernt: ein langsamerer stationärer Zustand, erkauft mit einer schnelleren ersten Nachricht. Pinnen Sie zuerst die besitzenden Threads, dann lassen Sie jeden von ihnen berühren, was er verwenden wird.
Fehlermodus-Prüfung: Wurde jede Seite, die dieser Pfad berühren wird, von dem Thread, der sie lesen wird, geschrieben, und was verhindert, dass sie zurückgefordert wird? Drei Fragen, denn die Beantwortung nur der ersten hinterlässt ein System, das schnell ist, bis es untätig wird, und die Beantwortung nur der ersten beiden hinterlässt eines, das auf einem einzelnen Socket schnell ist.
[R:keine-allokation-auf-dem-heissen-pfad] ist das Geschwisterkind, und keines subsumiert das andere – dieses hier betrifft das Betreten des Allokators, jenes den Speicher, den er bereits zurückgegeben hat. [R:beantworte-die-anforderung-auf-ihrer-ebene] ist der Grund, warum die Abhilfen alle eine Speicherrichtlinie, ein Lock oder ein Boot-Parameter sind: Keine Wahl eines Containers oder einer Crate erreicht dies. [R:kein-stau-innerhalb-eines-veroeffentlichungsfensters] ist, wo es am härtesten zubeißt, weil ein Seitenfehler, der zwischen zwei Speichervorgängen eines Writers auftritt, das Fenster für seine gesamte Dauer offen hält.
[R:beantworte-die-anforderung-auf-ihrer-ebene] Beantworten Sie eine Anforderung auf der Ebene, auf der sie lebt [R:answer-the-requirement-at-its-layer]
Geschrieben für Rust- und Java-Zielgruppen.
Geben Sie an, auf welcher Ebene eine Anforderung lebt, bevor Sie etwas wählen, und erfüllen Sie sie dort.
Eine Anforderung über die Maschine – stören Sie keinen benachbarten isolierten Kern, lösen Sie keinen Seitenfehler auf dem Sendepfad aus, nehmen Sie keinen Lock, um den ein Echtzeit-Thread konkurriert – wird nicht durch die Wahl einer schnelleren Crate, eines renommierteren Transports oder einer asynchronen API beantwortet. Das sind Entscheidungen auf Bibliotheksebene. Sie mögen korrekt sein und sie können sogar helfen, aber sie halten keine Einschränkung auf Systemebene, und sie so zu behandeln, als täten sie es, verwandelt eine offene Frage in eine geschlossene mit nichts dahinter.
Der Fehler ist nicht die falsche Wahl; es ist der falsche Eintrag im Hauptbuch. Eine als erfüllt markierte Einschränkung wird nicht mehr untersucht. Eine offene Einschränkung ist noch nicht erfüllt. Wenn Sie eine Einschränkung auf Systemebene mit einer Wahl auf Bibliotheksebene als erfüllt markieren, haben Sie die Frage nicht beantwortet; Sie haben sie unsichtbar gemacht.
Die Regel ist eine prozedurale: Nennen Sie die Ebene – Maschine, Betriebssystem, Laufzeit, Bibliothek, Anwendung – und zeigen Sie, dass die Antwort auf derselben Ebene liegt. Die Frage auf Systemebene „Darf der Sendepfad keinen Seitenfehler verursachen?“ wird nicht durch die Antwort auf Anwendungsebene „Wir verwenden keine dynamischen Allokationen mehr“ beantwortet, wenn die Antwort auf Laufzeit- oder Betriebssystemebene „Dann tue ich es trotzdem“ lautet. Ein Seitenfehler kann durch alles von der Seitenwiederherstellung über die Heap-Kompaktierung bis zur transparenten Huge-Page-Defragmentierung verursacht werden. Keine davon ist eine Anwendungsentscheidung.
Fehlermodus-Prüfung: Auf welcher Ebene lebt diese Einschränkung, und auf welcher Ebene wird sie erfüllt? Wenn die Antworten unterschiedlich sind, haben Sie das Problem entweder auf eine niedrigere Ebene verschoben oder ein Etikett darauf geklebt und es nicht gelöst. [R:kein-syscall-auf-einem-begrenzten-pfad] ist der direkteste Nachkomme dieser Regel, und [R:alloziert-ist-nicht-resident] ein anderer.
[R:keine-allokation-auf-dem-heissen-pfad]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus der Arbeit an Systemen, in denen Allokation entweder einen Lock oder einen Seitenfehler bedeutet; beide sind zu teuer, um auf einem Zeit-kritischen Pfad zu erscheinen.
Allokieren Sie alles, was ein Pfad benötigt, bevor er aufgerufen wird. Der Pfad selbst darf keine Zuweisung auslösen.
Auf dem heißen Pfad: kein malloc, kein new, keine impliziten Allokationen durch Bibliotheken (z. B. String::from, Vec::push ohne vorherige Reservierung, Box::new, HashMap-Einfügungen). Allein die Möglichkeit eines Seitenfehlers oder einer Lock-Konkurrenz macht die Latenz nicht deterministisch.
Ausnahmen: Wenn die Allokation nachweislich aus einem vorab allokierten Pool erfolgt (z. B. Objektpool, Arena-Allokator, Freeliste), ohne Seiteneffekte, die die Latenz beeinflussen. Aber „nachweislich“ ist der Schlüssel. Ein Pool, der unter Last wächst, ist kein Pool.
Fehlermodus-Prüfung: Welche Allokationen treten auf diesem Pfad auf, und wann wurden sie vorbereitet? Wenn die Antwort „während des Pfades“ und „nicht“ ist, ist dies ein Problem.
[R:kein-koordiniertes-auslassen]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus Gil Tene, „Wie man die Latenz nicht misst“ (2013) – der Fehler, bei dem das Instrument Stichproben nur während gutartiger Intervalle nimmt und die Ereignisse auslässt, die die Latenz verursachen.
Stellen Sie sicher, dass die Messung Stichproben während der gesamten Ausführungsdauer nimmt, einschließlich der Intervalle, in denen das System unter Last steht oder blockiert ist. Vermeiden Sie, dass die Metrik nur dann erfasst wird, wenn der Pfad nicht ausgelastet ist.
Koordiniertes Auslassen tritt auf, wenn der Lastgenerator wartet, bis das System bereit ist, bevor er die nächste Anfrage sendet, sodass die gemessenen Antwortzeiten nur die gutartigen Intervalle abdecken. Die Lösung ist ein Lastgenerator, der ohne Pause sendet (z. B. Fixed-Pace oder Closed-Loop mit einer statischen Anzahl von Threads).
Fehlermodus-Prüfung: Sendet der Lastgenerator unabhängig vom Systemstatus? Wenn er auf Antworten wartet, misst er nur den Teil, der funktioniert.
[R:kein-false-sharing-auf-einer-heissen-zeile]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus der Cache-Kohärenz-Theorie in Computerarchitektur (z. B. Hennessy & Patterson). False Sharing tritt auf, wenn zwei Threads auf verschiedene Variablen schreiben, die sich auf derselben Cache-Zeile befinden, was unnötige Cache-Kohärenz-Nachrichten verursacht.
Stellen Sie sicher, dass Thread-lokale oder von verschiedenen Threads frequentierte Variablen nicht auf derselben Cache-Zeile (typischerweise 64 Byte) liegen. Verwenden Sie Padding (z. B. Felder mit #[repr(align(64))] in Rust, @Contended in Java, oder manuelles Auffüllen mit Dummy-Feldern).
False Sharing tritt nicht nur bei Standard-Threads auf; es betrifft auch Pseudo-Threads wie Green-Threads, wenn die zugrundeliegenden Variablen im gemeinsamen Speicher liegen.
Fehlermodus-Prüfung: Schreiben zwei Threads auf Variablen innerhalb von 64 Byte voneinander entfernt? Wenn ja, liegt False Sharing vor.
[R:keine-wiederholungsschleife-auf-einem-umkaempften-pfad]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus der Beobachtung, dass eine Wiederholungsschleife (Retry-Loop) auf einem umkämpften Pfad (z. B. CAS in einer Schleife unter hohem Druck) die Latenz verschlechtert, da verbrauchte Zeit vergeudet wird, während keine Arbeit erledigt wird.
Verwenden Sie keine Spin-Loops oder CAS-Schleifen als primäre Synchronisationsstrategie, wenn Konkurrenz zu erwarten ist. Weichen Sie auf Backoff (z. B. yield, Backoff::spin in Rust) oder besser auf eine blockierende Strategie (Mutex, Sperre) aus, die den Thread schlafen legt, wenn der Pfad umkämpft ist.
Ausnahme: Wenn die Wiederholungsschleife nachweislich kurz und selten ist (z. B. atomare Operationen in einem lockfreien Design), aber selbst dann muss die Latenz deterministisch sein.
Fehlermodus-Prüfung: Gibt es eine Schleife, die auf einen atomaren Erfolg wartet, und ist der Pfad jemals unter Last umkämpft? Wenn ja, kann die Latenz verheerend sein.
[R:kein-stau-innerhalb-eines-veroeffentlichungsfensters]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus der Arbeit an Lock-Free- und Wait-Free-Fenstern: Ein Veröffentlichungsfenster ist die Zeitspanne, in der ein Writer mehrere Speichervorgänge ausführt, die zusammen einen konsistenten Zustand definieren. Ein Stau (z. B. Seitenfehler, Syscall, Lock) innerhalb dieses Fensters hält es offen für Leser.
Stellen Sie sicher, dass innerhalb eines Veröffentlichungsfensters (z. B. dem Schreiben von Head- und Tail-Zeigern in einer SPSC-Queue) keine Operationen ausgeführt werden, die einen Seitenfehler, einen Syscall (z. B. mmap, read, write), einen Lock oder andere Operationen mit nicht-deterministischer Latenz verursachen.
Ein Veröffentlichungsfenster ist die kritische Phase, in der ein Writer Daten bereitstellt, die für andere Threads konsistent werden. Ein Stau hier kann zu inkonsistenten Zuständen oder erhöhter Latenz führen.
Fehlermodus-Prüfung: Wie breit ist das Veröffentlichungsfenster, und enthält es Operationen mit nicht-deterministischer Latenz? Wenn ja, ist es ein Problem.
[R:kein-syscall-auf-einem-begrenzten-pfad]
Geschrieben für Rust- und Java-Zielgruppen.
Niedergeschrieben aus der Beobachtung, dass ein Syscall (z. B.
read,write,poll,lock) auf einem Pfad mit einer Latenzgrenze diese Grenze bricht, da der Kernel keine Latenzgarantie gibt.
Vermeiden Sie Systemaufrufe auf Pfaden mit einer Latenzgrenze. Pre-allozieren Sie alles, was der Pfad benötigt (z. B. Puffer für Netzwerk-I/O, Dateideskriptoren für Pooling). Wenn ein Syscall unvermeidbar ist, stellen Sie sicher, dass er außerhalb des Pfades oder nur unter bestimmten Bedingungen auftritt (z. B. beim ersten Start).
Beispiele: Verwenden Sie io_uring (Linux) für asynchrone E/A ohne Syscall auf dem heißen Pfad; verwenden Sie vorab allozierte Puffer für Netzwerk-Reads.
Fehlermodus-Prüfung: Welche Systemaufrufe treten auf diesem Pfad auf, und können sie vermieden oder vorab erledigt werden?