1 Punkte von GN⁺ 2 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • assert ist ein Mittel, um Vorbedingungen, Nachbedingungen und Invarianten im Code festzuhalten; Einschränkungen, die sich über das Typsystem erzwingen lassen, sollten vorzugsweise mit Sprachfunktionen ausgedrückt werden
  • Zigs std.debug.assert ist kein Makro, sondern eine normale Funktion, die mit unreachable unerreichbare Pfade markiert und auch für Optimierungen genutzt wird
  • In Debug und ReleaseSafe führt ein fehlgeschlagenes assert per panic zum Absturz, in ReleaseFast und ReleaseSmall kann es hingegen als unchecked illegal behavior zu Fehlverhalten kommen
  • Wer asserts in Produktion abschaltet, verliert die Chance, falsche Annahmen früh zu entdecken; später kann Code dann von falschen asserts abhängen und in Schwachstellen münden
  • Ob man ReleaseSafe oder ReleaseFast wählt, hängt von den Prioritäten des Programms ab; entscheidend ist aber, asserts nicht pauschal abzuschalten, sondern falsche asserts zu korrigieren

Rolle von assert und Zigs Standardverhalten

  • assert ist ein Mittel, um im Code auszudrücken, dass Bedingungen wie „dieses Argument kann nicht null sein“ oder „diese Ganzzahl kann nicht gerade sein“ immer wahr sein müssen
    • Beispiel: assert(my_arg != null);, assert(my_num % 2 != 0);
    • Wenn sich eine Einschränkung über das Typsystem erzwingen lässt, ist eine Sprachfunktion einem assert vorzuziehen
    • In Zig kann ein gewöhnlicher Pointer *Foo nicht null sein, während ein optionaler Pointer ?*Foo null sein darf, aber vor dem Zugriff eine Prüfung erzwingt
  • assert eignet sich zur Beschreibung von Vorbedingungen, Nachbedingungen und Invarianten
    • Gute asserts können Programmierfehler wirkungsvoller aufdecken als Unit-Tests
    • In Kombination mit Fuzzing kann die Wirkung von asserts noch größer sein

Zigs unreachable und assert

  • Zigs assert basiert auf unreachable, einer Sprachfunktion zur Markierung falscher Codepfade
    • In einem switch lassen sich unerreichbare Zweige etwa als .a => unreachable markieren
    • unreachable kann sowohl als Statement als auch dort verwendet werden, wo ein Ausdruck beliebigen Typs erwartet wird
    • Es ist nicht nötig, für unerreichbare Fälle künstlich einen Platzhalterwert zu erzeugen
  • std.debug.assert in der Zig-Standardbibliothek ist so implementiert
    pub fn assert(ok: bool) void {
      if (!ok) unreachable; // assertion failure
    }
    
  • Die Information aus unreachable kann für Optimierungen genutzt werden
    • Der Compiler kann unerreichbare Pfade entfernen, diese Information weiterreichen und dadurch nichtlokale Optimierungen durchführen
    • Nicht jedes assert führt zu mehr Leistung, aber es sind auch Optimierungen möglich, die Programmierer nicht leicht vorhersehen

Build-Modi und Laufzeitsicherheit

  • Zig kennt die Build-Modi Debug, ReleaseSafe, ReleaseFast und ReleaseSmall
    • Diese Einstellung muss nicht zwingend global für das gesamte Programm gelten
    • Abhängigkeiten können jeweils in unterschiedlichen Modi gebaut werden, und mit @setRuntimeSafety lässt sich die Laufzeitsicherheit sogar blockweise innerhalb einer Funktion anpassen
  • Ein fehlgeschlagenes assert gilt in Zig als „illegal behavior“
    • In den checked Modi Debug, ReleaseSafe und @setRuntimeSafety(true) stürzt das Programm mit panic ab
    • In den unchecked Modi ReleaseFast, ReleaseSmall und @setRuntimeSafety(false) tritt „unchecked illegal behavior“ auf, wodurch das Programm falsch arbeiten kann
  • Das Ergebnis von unchecked illegal behavior ist nicht garantiert
    • Im gezeigten switch kann es wegen der aktuell erzeugten Maschinencodes so aussehen, als würde in einen anderen Zweig gesprungen
    • Mit einer anderen Compilerversion kann das Fehlverhalten völlig anders aussehen
    • Das zugehörige Verhalten ist im godbolt-Beispiel zu sehen
  • Wie sich assert und ein nachfolgendes switch in ReleaseSafe und ReleaseFast unterscheiden, zeigt ein weiteres godbolt-Beispiel
    • In ReleaseFast kann die Funktion am Ende alle Vergleiche überspringen und einfach true zurückgeben
    • Auf genau diese Art von Optimierung sind Videospiele und andere Echtzeit-Medienanwendungen stark angewiesen

Zig-assert ist kein Makro

  • Zigs std.debug.assert ist kein Makro, sondern eine normale Funktion
    • Zig hat keine Makros
    • Gerade für C/C++-Entwickler ist das beim Einstieg in Zig oft überraschend
  • In C/C++ ist es üblich, dass beim Deaktivieren von asserts der gesamte assert-Aufruf mitsamt dem übergebenen Ausdruck so behandelt wird, als wäre er auskommentiert
    • Deshalb sollten in C/C++ keine Ausdrücke mit Seiteneffekten in asserts stehen
    • Wird assert deaktiviert, kann die betreffende Operation selbst verschwinden
  • In Zig werden nach den Regeln für Funktionsaufrufe die Argumente vor dem Funktionsaufruf ausgewertet
    • Unabhängig von der internen Logik von std.debug.assert wird der Ausdruck des Arguments ausgewertet
    • Deshalb kann man auch Ausdrücke mit Seiteneffekten in assert verwenden, etwa so
    // assert that the remove operation is not a noop:
    assert(my_map.remove("expected-to-exist"));
    
  • Umgekehrt gilt: Wenn die Berechnung der assert-Bedingung komplexe Operationen erfordert, werden diese im unchecked Modus nicht zwingend entfernt
    • In solchen Fällen sollte der Code mit comptime if geschützt werden
    const builtin = @import("builtin");
    
    if (builtin.mode == .Debug) {
      var condition = ...;
      // whatever bookkeeping is necessary
      // to compute the condition
      assert(condition == .ok);
    }
    
  • Wer an die Semantik aus C/C++ gewöhnt ist, findet das möglicherweise ungewohnt, aber in Zig ist implizit vorausgesetzt, dass asserts normalerweise nicht deaktiviert werden

Das Problem mit deaktivierten asserts in Produktion

  • Bei asserts gibt es im Wesentlichen drei Möglichkeiten
    • Man belässt sie als Laufzeitprüfung und lässt den Prozess bei einem Fehlschlag per panic abstürzen
    • Man nutzt asserts für Performance-Optimierungen und nimmt Fehlverhalten in Kauf, wenn ein assert falsch ist
    • Man deaktiviert asserts vollständig
  • std.debug.assert unterstützt die vollständige Deaktivierung von asserts standardmäßig nicht
    • Mit einem eigenen assert, das intern ein Build-Flag prüft, lässt sich ein Verhalten näher an C/C++ nachbauen
  • Der Wunsch, asserts abzuschalten, entsteht meist aus einer Kombination zweier Gründe
    • Man möchte die Laufzeitprüfung wegen Kosten oder Abstürzen der Anwendung nicht beibehalten
    • Man vertraut nicht darauf, dass asserts immer korrekt sind, und fürchtet Fehlverhalten, wenn sie für Optimierungen verwendet werden
  • Wie matklad in der zugehörigen Diskussion in Erinnerung gerufen hat, gibt es Situationen mit legitimen technischen Gründen, Abstürze zu vermeiden
    • Für allgemeine Software Absturzvermeidung zum Standard zu machen, wird jedoch als schlechte Wahl bewertet
  • Wer asserts deaktiviert, sorgt dafür, dass Bedingungen, die angeblich unmöglich sind, beim tatsächlichen Auftreten das Programm nicht stoppen
    • Das Programm läuft unter falschen Annahmen weiter, was bereits eine Form von Fehlverhalten ist, auch wenn es sich nicht um unchecked illegal behavior handelt
  • Unchecked illegal behavior oder undefiniertes Verhalten in C sind deshalb gefährlich, weil sie das Programm in eine weird machine verwandeln können
    • In ausreichend komplexer Software kann es auch ohne UIB zu unbeabsichtigten, verdrehten Ausführungspfaden kommen
    • Wenn ein assert zur Laufzeit falsch wird, bedeutet das einen Verstoß gegen die Spezifikation und kann allein dadurch unbeabsichtigte Aktionen auslösen
    • SQL-Injection ist ein konkretes, weitverbreitetes Beispiel für weird-machine-artiges Fehlverhalten ganz ohne UIB
  • Wenn die Kosten von Fehlverhalten zu hoch sind, sollte man asserts eingeschaltet lassen
    • Wenn Performance extrem wichtig ist und das Risiko von Fehlverhalten tragbar erscheint, ist es sinnvoller, asserts als Optimierungschance zu nutzen
    • Wer asserts deaktiviert, verzichtet auf Performance und wiegt sich zugleich leicht in falscher Sicherheit

Wie falsche asserts eine Codebasis täuschen

  • Das zentrale Risiko besteht darin, dass falsche asserts in Tests unbemerkt bleiben und erst in Produktion fehlschlagen können
    • Wenn garantiert wäre, dass alle asserts immer wahr sind, wäre ihr Einsatz zur Optimierung nicht umstritten
    • Wenn garantiert wäre, dass Tests alle falschen asserts finden, wären auch Produktionsoptimierungen sicher
    • In der Praxis kann man jedoch falsche asserts schreiben, und Tests decken sie nicht zwingend auf
  • Wer asserts in Produktion abschaltet, verliert die Chance, falsche asserts möglichst früh zu entdecken
    • Noch gravierender ist, dass späterer Code weiterhin unter Abhängigkeit von diesen falschen asserts geschrieben wird
  • Im Beispielcode wird per assert angenommen, dass processThing nur mit einem bereits gestarteten thing aufgerufen werden darf
    fn processThing(thing: Thing) void {
       // this function must always be invoked on
       // a thing that has already been started
       assert(thing.is_started);
    
       // ...
    }
    
  • Dieses assert kann in Tests niemals fehlschlagen, während es in Produktion deaktiviert ist und dort tatsächlich falsch werden könnte
    • Solange kein für Nutzer sichtbares Fehlverhalten auftritt, scheint alles in Ordnung zu sein, und die Entwicklung geht weiter
  • Später könnte jemand Code hinzufügen, weil thing ja bereits gestartet sei und man daher baz ohne zusätzliche Vorbereitung aufrufen könne
    fn processThing(thing: Thing) void {
       // this function must always be invoked on
       // a thing that has already been started
       assert(thing.is_started);
    
       // ...
    
       // Since thing is already started, we don't
       // need to foo the bar before bazzing the qux.
       // It would be really bad to baz the qux otherwise,
       // so we add an assert for good measure.
       assert(thing.is_fooed);
       thing.baz(qux);
    }
    
  • Auch wenn das zweite assert logisch korrekt sein mag, entsteht ein Risiko, wenn das erste assert in Wirklichkeit falsch werden kann
    • In Tests schlägt das erste assert nicht fehl, also auch das zweite nicht
    • In Produktion sind asserts deaktiviert, sodass der Moment, in dem die Schwachstelle in die Codebasis gelangt, unbemerkt bleiben kann
  • Wenn asserts im Code Entwickler in die Irre führen, wird es unangemessen schwer, korrekten Code zu schreiben

Die Wahl hängt von den Prioritäten des Programms ab

  • Jedes Programm hat andere Prioritäten, und bei manchen ist es legitim, Performance höher zu gewichten als die Minimierung von Fehlverhaltensrisiken
    • In diesem Fall ist es naheliegend, asserts in Optimierungsmöglichkeiten umzuwandeln
  • Das träge, gewohnheitsmäßige Deaktivieren von asserts in Produktion wird als schlechtere Wahl bewertet als sowohl eingeschaltete asserts als auch die aktive Nutzung von Performance-Optimierungen
    • Eine Haltung, die ReleaseFast stark kritisiert, aber das Deaktivieren von asserts kritiklos hinnimmt, ist widersprüchlich
  • Zine ist ein statischer Site-Generator und wird derzeit vor allem zum Bauen persönlicher Blogs genutzt
    • Das Bedrohungsmodell ist nicht definiert, und das ist auch nicht die höchste Priorität
    • Daher werden ReleaseFast-Builds ausgeliefert, weil sie etwa eine Größenordnung schneller laufen als Hugo
  • Awebo ist eine selbst hostbare Discord-Alternative im Pre-Alpha-Stadium
    • Es ist bereits klar, dass die Software personenbezogene Daten verarbeitet und dem Internet ausgesetzt sein wird
    • Deshalb sollen zur Auslieferung ReleaseSafe-Builds bereitgestellt werden
    • Einige zentrale Abhängigkeiten wie FFmpeg, Xiph Opus und SQLite sollen jedoch in ReleaseFast gebaut werden
    • Dort wird der Performancegewinn klar höher bewertet als die weitere Reduktion des Risikos von Fehlverhalten

Entscheidungen realer Projekte und Sicherheitsfälle

Implizite asserts, die in Zig nicht vollständig verschwinden

  • Auch wenn sich eigene asserts deaktivieren lassen, können die impliziten asserts, die Zig selbst dem Code hinzufügt, nicht abgeschaltet werden
    • Dazu gehören Integer-Überlauf, Division durch null oder Zugriffe außerhalb von Array-Grenzen
    • Solche Bedingungen lösen entweder zur Laufzeit einen panic aus oder werden für Optimierungen genutzt
  • Die Praxis, Produktions-asserts zu deaktivieren, kann dazu führen, dass falsche asserts in der Codebasis verrotten und sich vermehren
    • Das verstärkt am Ende die Paranoia gegenüber UIB, und Entwickler könnten unbewusst Angst davor entwickeln, asserts wieder einzuschalten und sich den Konsequenzen zu stellen
  • Die unausweichliche Schlussfolgerung lautet daher nicht, asserts abzuschalten und zu überdecken, sondern falsche asserts zu korrigieren
    • Korrektheit sollte für das gesamte Programm angestrebt werden, nicht nur für irgendeine Teilmenge davon

1 Kommentare

 
GN⁺ 2 시간 전
Lobste.rs-Kommentare
  • Ich stimme zu, dass es bei assert im Allgemeinen am besten ist, einfach zum Absturz zu bringen oder wie bei Rust nur die jeweilige Aufgabe abstürzen zu lassen. Aber ich finde es schwer zuzustimmen, dass die Nutzung von assert als Optimierungs-Hinweis immer besser ist, als es einfach wegzulassen.
    Erstens helfen beliebige asserts der Optimierung oft kaum, und viele Bedingungen kann der Optimierer nicht direkt verwerten. Wenn man nicht eine direkte Annahme wie „Dieser Zweig wird niemals erreicht“ einfügt, ist der Performancegewinn vermutlich nicht groß, wenn man überall im Code zufällige Annahmen verstreut.
    Zweitens vergrößert sich der Schadensradius von Fehlern erheblich, wenn man assert in Annahmen umwandelt. Nehmen wir zum Beispiel ein System, das Daten verarbeitet, die nach Projekt oder Nutzer getrennt sind, und mitten in einer Berechnungsfunktion steht ein assert, das einen Zustand abfängt, der eigentlich unmöglich sein sollte. Wenn man ihn im Release-Build wegen der Kosten deaktiviert, dann bleibt der Schaden bei bloßer Deaktivierung auf ein Projekt oder einen Nutzer begrenzt und wird vielleicht bei einer späteren Prüfung noch erkannt. Macht man daraus dagegen undefiniertes Verhalten, kann die Berechnung an völlig falschen Code springen, den Speicher beliebig beschädigen und die Daten aller Projekte zerstören.
    Wenn man also unsichere asserts zum Standard im Release-Build macht, optimiert man letztlich voreilig beliebige Stellen des Codes und verringert dafür die Chance, Schäden im Problemfall zu lokalisieren. Ich finde, Rust ist hier gut designt: assert!() führt immer zu einem Panic, debug_assert!() nur im Debug-Modus, und assert_unchecked() führt im Debug-Modus zu einem Panic und wird im Release-Modus zu einem Optimierungs-Hinweis.

    • Wenn man sich um den Schadensradius von Fehlern sorgt, sollte man statt ReleaseFast lieber ReleaseSafe verwenden.
    • Ich bin nicht dagegen, einzelne asserts zu deaktivieren, sondern dagegen, sie wie eine allgemeine empfohlene Praxis pauschal abzuschalten.
      Wenn man entscheidet, dass der Performance-Einfluss zu groß ist, um sie im Release-Build beizubehalten, ist das völlig vernünftig. Außerdem führen teure asserts, wie oben gesagt, mit hoher Wahrscheinlichkeit ohnehin nicht zu spürbaren Performanceverbesserungen.
      Auch in Zine gibt es einige solche Beispiele:
      https://github.com/kristoff-it/zine/…
      https://github.com/kristoff-it/zine/…
      Zig hat keinen „standardmäßigen Release-Modus“. Man muss immer selbst wählen, wie assert behandelt werden soll, und die globalen Optionen sind Absturz oder Optimierung; keine von beiden kann wirklich als der eigentliche Standard gelten.
  • Dass die beiden bislang veröffentlichten vergleichsweise schwerwiegenden CVEs in Ghostty beide ohne Speicherbeschädigung zu beliebiger Befehlsausführung führten, wirkt auf mich sehr merkwürdig. Dass das trotz Verteilung mit ReleaseFast passiert ist, widerspricht meinem Verständnis davon, wie die Welt funktioniert.

    • So merkwürdig finde ich das nicht. Selbst wenn man dem Bericht glaubt, dass 70 % der schweren Schwachstellen speicherbezogen sind, gilt das für C und C++, und Zig könnte bei Speichersicherheit etwas besser sein. Außerdem ist es bei einer Stichprobe von 2 nicht ungewöhnlich, wenn etwa eines von zehn Projekten so ein Ergebnis zeigt.
      Aus meiner Erfahrung mit Terminalemulatoren sind diese Schwachstellen genau die erwartbare Art von lästigen Problemen. Das soll Entwickler oder Forscher nicht herabsetzen, aber solche Command-Injection an unerwarteten Stellen gehört in diesem Bereich fast schon dazu, ähnlich wie in anderen Domänen andere Injection-Schwachstellen häufig dazugehören.
  • Ich finde es lustig, dass ich seit fast 40 Jahren höre, man solle asserts und Grenzprüfungen in Produktion „aus Performancegründen“ abschalten. In dieser Zeit sind Computer um mehrere Größenordnungen schneller geworden, und Software ist viel tiefer in das Leben aller Menschen eingedrungen, daher ist die Korrektheit zur Laufzeit heute wichtiger denn je.
    Konstruktiver gesprochen: Beim alten Microsoft gab es neben gewöhnlichen asserts und checks etwas, das ich anderswo kaum gesehen habe: Reporting-Assertions. Die verwendet man, wenn es Bedingungen gibt, die man nicht vollständig kontrolliert, von denen man aber annimmt, dass sie wahr sind, die man im falschen Fall defensiv behandelt und bei denen man per Logging oder Telemetrie wissen möchte, ob sie in der Praxis tatsächlich falsch werden. Zum Beispiel, wenn man annimmt, dass Nutzer nie mehr als 1000 Einträge in eine Liste packen und deshalb einen quadratischen Algorithmus verwendet, oder wenn man davon ausgeht, dass die Netzwerklatenz unter 200 ms liegt und deshalb ein Protokoll mit vielen Roundtrips einsetzt.

    • Worin unterscheidet sich das von check?
  • Als einer der hier verlinkten Beteiligten kann ich sagen: Das macht aus meiner Sicht auf assert eine lächerliche falsche Dichotomie und eine Karikatur. Wie ich auch in einem anderen Kommentar geschrieben habe, entscheide ich lieber pro assert, ob in undefiniertes Verhalten überführt werden soll. Meine Kritik an ReleaseFast ist, dass diese Entscheidung nicht nur für alle asserts in einem bestimmten Bereich, sondern auch mit sämtlichen Sicherheitsprüfungen zusammengebunden wird.
    Ich stimme kristoff zu, dass es dumm ist, nicht korrigierte asserts nur deshalb abzuschalten, weil sie Abstürze verursachen. Aber ich stimme nicht zu, dass „Absturz oder undefiniertes Verhalten“ die einzigen vernünftigen Alternativen seien. Die Position von goldstein im Schwesterkommentar liegt meiner Ansicht nach näher an dem, was ich denke.

  • Es ist schwer zu verteidigen, das Verhalten von assert_unchecked() zum globalen Standard zu machen, aber als Performance-Optimierung kann es sinnvoll sein. Wenn ein Produktions-Build deutlich schneller wird, sobald man alle asserts in Annahmen umwandelt, dann könnte es eine kleine Zahl von Annahmen, hoffentlich sogar nur eine einzige, geben, die den Großteil der Verbesserung ausmacht, und man könnte sie mit Methoden wie binärer Suche finden.

    • Es gibt keinen Standardwert; der Nutzer wählt explizit zwischen ReleaseSafe und ReleaseFast/ReleaseSmall.
  • In der Programmanalyse-Literatur gibt es eine Dualität, die Behauptungen im Code oder assert in zwei Formen einteilt. Die eine betrifft den Kontext um den Code herum, bei einer Funktion also die Bedingung, die der Aufrufer erfüllen muss, und die andere den Code selbst, bei einer Funktion also die Bedingung, die die Funktion erfüllen muss.
    Diese Unterscheidung wird klar, wenn man sie unter dem in der Vertrags- und Gradual-Typing-Literatur üblichen akademischen Begriff der „Verantwortung“ (blame) betrachtet. Wenn eine Behauptung über den Kontext fehlschlägt, liegt der Fehler nicht bei uns, sondern die Verantwortung beim Kontext oder Aufrufer; es kann aber auch sein, dass der Aufrufer korrekt ist und die Behauptung selbst fehlerhaft ist. Wenn eine Behauptung über den Code selbst fehlschlägt, liegt die Verantwortung bei uns; es kann aber auch sein, dass der Code korrekt ist und die Behauptung selbst fehlerhaft ist.
    Auf Funktionsebene ist eine Vorbedingung eine Behauptung über den Kontext und eine Nachbedingung eine Behauptung über den Code selbst. Beide können allerdings auch mitten im Code platziert werden. Manche Verifikations-Frameworks verwenden assert für Behauptungen über den Code und assume für Behauptungen über den Kontext. Das hängt auch damit zusammen, wie einige Test-Frameworks, insbesondere Frameworks für zufällige Tests, dies interpretieren. Wenn assert fehlschlägt, wird der Test als fehlgeschlagen markiert; wenn assume fehlschlägt, wird der Test übersprungen.

    • BIND9 folgt mit dem Makro REQUIRE() für Vorbedingungen, die der Aufrufer erfüllen muss, und ENSURE() für Nachbedingungen, die die Funktion garantiert, einem Stil, der Design by Contract nahekommt. Für Prüfungen innerhalb des Codes gibt es INSIST(), für Schleifen oder Datenstrukturen INVARIANT(). In der Funktionsdokumentation sollten „requires“- und „ensures“-Vermerke stehen, die diesen Vor- und Nachbedingungen entsprechen.
  • Das scheint auf Bun anzuspielen, deshalb würde ich die Verbindung gern etwas formeller festhalten. Es gibt ein Zig-Issue aus dem Jahr 2024, in dem Bun-Ersteller Jarred Sumner vorgeschlagen hat, dass unreachable in ReleaseFast paniken sollte. Die Kommentare von Andrew Kelley und Matthew Lugg in diesem Thread sind für diese Diskussion relevant.
    => https://github.com/ziglang/zig/issues/19664
    Bun verwendet eigene assert-Funktionen, die im Release-Modus paniken oder entfernt werden, aber kein undefiniertes Verhalten einführen. Man sollte allerdings auch Loris' Fußnote im Kopf behalten: „Als Sprache fügt Zig dem Code implizit viele assert hinzu, die nicht deaktiviert werden können.“
    Ich möchte nicht zu lange über Bun sprechen. Es ist schließlich ein einzelnes Projekt eines kleinen Teams. Der Kernpunkt ist: Wenn auch nur die geringste Sorge besteht, sollte man ReleaseSafe verwenden. ReleaseSafe hat zwar den Ruf, langsam zu sein, aber in meinen kleinen Zig-Projekten konnte ich keinen Benchmark-Unterschied zwischen ReleaseSafe und ReleaseFast messen. Wahrscheinlich ist es trotzdem noch schneller als viele andere Sprachen.

    • Es stimmt, dass man bei auch nur der geringsten Sorge ReleaseSafe verwenden sollte. Es sind aber auch interessantere Strategien möglich. Während man den Code verändert, also solange die Gefahr besteht, Bugs einzubauen, kann man bei ReleaseSafe bleiben und später, wenn sich der Code stabilisiert hat und sich in der Praxis bewährt hat, auf ReleaseFast wechseln, falls die zusätzliche Performance den Aufwand wert ist.
      Oder man liefert, wenn es im Kontext sinnvoll ist, zunächst ein ReleaseFast-Binary aus und wechselt zurück zu ReleaseSafe, sobald wegen undefiniertem Verhalten nichtdeterministische Bugreports eingehen. Dann kann man verwertbare Bugreports sammeln, etwa darüber, welches assert fehlgeschlagen ist, oder über Out-of-Bounds-Zugriffe und Overflows, und den Code beheben. Ich würde diesen Ansatz sogar empfehlen, wenn man sich ursprünglich in einem Kontext, in dem man gar nicht mit ReleaseFast hätte ausliefern sollen, dennoch dafür entschieden hat :^)
      Man kann auch Abhängigkeiten anpassen und mit @setRuntimeSafety dasselbe Vorgehen nur auf Teile eines Projekts anwenden. Letztlich sind alle nötigen Werkzeuge vorhanden, wenn man bereit ist, klug damit umzugehen.
  • Man sollte nicht so schreiben, als dürften in assert-Aufrufen Ausdrücke mit Seiteneffekten stehen. Das ist eine schlechte Praxis. Auch assert für Fehlerprüfungen zu verwenden, sollte man vermeiden. Fairerweise scheint der Autor das aber nicht zu behaupten.
    Umgekehrt wird auch erklärt, dass, wenn assert von aufwendigen Berechnungen abhängt, diese Berechnungen im unchecked-Modus nicht zwingend entfernt werden und man sie deshalb mit comptime if absichern sollte.
    Hoffentlich ist dem Autor die Ironie der Aussage nicht entgangen, dies sei „eine gute Gelegenheit, das von Makros hinterlassene Trauma loszulassen und Einfachheit zu akzeptieren“. Gemeint ist offenbar, man solle „die Einfachheit akzeptieren, den Build-Modus des Programms zu berücksichtigen und überall defensive comptime if zu verstreuen“.

    • Warum ist das eine schlechte Praxis?
  • Ich schreibe gelegentlich numerischen Code in C# und verwende viele assert, die in Release deaktiviert werden. Sie sind zu teuer, um sie in jeder dicht laufenden Schleife auszuführen, aber in Unit-Tests ist es nützlich, wenn eine Routine sofort abbricht, sobald sie zum ersten Mal NaN-Eingaben sieht.
    Solche NaNs entstehen oft nicht durch Benutzereingaben, sondern durch Bugs im Code, etwa wenn der Optimierer an Stellen gerät, an denen er nicht sein sollte, oder wenn bessere Randbedingungen nötig sind. Natürlich müssen Benutzereingaben womöglich validiert werden, aber das sollte an der äußersten Grenze geschehen, nicht tief im Algorithmus. Es wäre schön, ein Beweissystem zu haben, das als Ergebnis der Validierung von Benutzereingaben Invarianten im Inneren des Algorithmus auch ohne assert beweisen kann, aber das ist ein Nebenprojekt, und wenn es abstürzt, stirbt niemand.

  • 90 % der Meinungsverschiedenheiten über assert entstehen dadurch, dass das Wort schlecht definiert ist und für mehrere Dinge verwendet wird; dadurch werden Denken und Kommunikation unscharf. Deshalb sollte man das Konzept in die folgenden drei Begriffe aufteilen und diese strikt verwenden.
    assert(bool) oder in Rust assert_unchecked() ist etwas, das der Programmierer für immer wahr hält und das der Compiler ebenfalls als immer wahr annimmt und für Optimierungen nutzt. Um Assoziationen mit prüfenden Assertions älterer Sprachen zu vermeiden, wäre assume() vielleicht die bessere Bezeichnung.
    check(bool) bedeutet: Wenn die Bedingung falsch ist, wird gepanikt, und wenn sie wahr ist, läuft das Programm weiter; und genau so verhält es sich immer.
    debug_check(bool) entspricht im Debug-Modus check() und läuft im Release-Modus immer weiter. In der Praxis wird das über ein Flag --debug_checks gesteuert, das im Debug-Modus standardmäßig aktiviert ist.
    Dazu braucht es auch ein Compiler-Flag --check_asserts, das assert() in check() umwandelt. Das verwendet man, wenn man die eigenen assert anzweifelt und sie verifizieren möchte; im Debug-Modus ist es standardmäßig aktiviert. Solange man nicht ganz klar sagt, was man mit „assert“ meint, ist eine reife Diskussion unmöglich, und man verschwendet nur Worte.