- Die für Kryptocode wichtige Eigenschaft der konstanten Laufzeit (constant-time) kann schon allein durch Compiler-Optimierungen zerstört werden; daher läuft ein Experiment, bei dem ein Warn-Patch in LLVM eingefügt wird, um riskante Muster zu finden
- Compiler-„Optimierungen“ können Teile von Benchmarks schneller machen, doch zentrale Hot Paths stützen sich in der Praxis oft auf Intrinsics und Assembly, und die Kosten von durch Optimierungen verursachten Bugs häufen sich separat an
- Im Juni 2024 bestätigte Antoon Purnal, dass der Kyber-Referenzcode unter Clang 15 und neuer bei bestimmten Optimierungsoptionen in eine bedingte Verzweigung auf Basis geheimer Werte umgewandelt wird und damit Timing-Angriffe ermöglichen kann
- TIMECOP 2 prüft innerhalb von SUPERCOP als konstantzeitlich deklarierte Compiler-Ergebnisse, hat aber Grenzen bei von Valgrind unterstützten Befehlen und bei Datenflüssen, die erst in tatsächlichen Testläufen sichtbar werden
- Praktische Gegenmaßnahmen bestehen darin, mit Funktionen wie
crypto_{int,uint}{8,16,32,64}.h zu verhindern, dass der Compiler 1-Bit-Ergebnisse als bool sieht, oder zu verifiziertem Assembly, sicherheitsorientierten Sprachen oder spezialisierten Compilern zu wechseln
Die Verantwortungslücke durch Compiler-„Optimierung“
- In den Änderungsverläufen aktueller Versionen von LLVM und GCC tauchen fortlaufend „Optimierungen“, „Optimierungs“-Tests, Testanpassungen und Bugfixes für „Optimierungen“ auf
- Wenn Code, der vor dem Kompilieren gut funktionierte, sich nach einer Compiler-Änderung anders verhält, wird die Verantwortung in vielen Fällen dem Programmierer zugeschoben, der „undefined behavior“ ausgelöst habe
- Solche „language standards“ werden von Compiler-Autoren erstellt; dadurch entsteht eine Struktur, in der der Code von Millionen Programmierern mehr Verantwortung trägt als die Änderungen einer kleinen Gruppe von Compiler-Autoren
- Als Beispiel aus Kryptocode ist die
avx2-Implementierung von kyber768 in mehreren CPU-Benchmarks etwa 4-mal schneller als portabler Code, der mit einem „optimierenden“ Compiler kompiliert wurde
Grenzen der Messung von Optimierungsleistung
- Todd A. Proebsting formulierte 2000 in Proebsting's Law, dass „Compiler-Fortschritte die Rechenleistung alle 18 Jahre verdoppeln“, und kam zu dem Schluss, dass Compiler-Optimierungen nur einen randständigen Beitrag leisten
- Arseny Kapoulkine fasste 2022 in einem Benchmark zusammen, dass LLVM 11 für optimiertes Kompilieren doppelt so lange braucht wie LLVM 2.7 und der ausgeführte Code im Allgemeinen 10 bis 20 % schneller ist
- Beide Diskussionen verfehlen die Performance-Messung, die tatsächliche Nutzer wahrnehmen
- In Hotspots, in denen sich Performance konzentriert, stecken häufig Intrinsics und Assembly
- FFmpeg enthält gemessen an
.asm- und .S-Dateien 160.000 Zeilen Assembly
- Je mehr Daten Computer und Netzwerke verarbeiten, desto stärker entfällt die tatsächliche CPU-Zeit auf solche Hotspots
- Auch Sicherheitskosten wachsen in der Optimierungsdebatte separat an
- Deloitte berichtete, dass IT-Sicherheitsbudgets 2023 0,5 % des Unternehmensumsatzes ausmachten
- Zusammen mit der Zahl, dass der weltweite Gesamtumsatz von Unternehmen 2022 bei über 48 Billionen Dollar lag, könnte die Gesamtdimension bei Hunderten Milliarden Dollar liegen
- Allerdings gibt es den Vorbehalt, dass Deloittes 0,5 % ein einfacher Durchschnitt über Unternehmen sein könnten und nicht alle Unternehmen an der Umfrage teilgenommen haben
Timing-Leaks und der Kyber-Fall
- Sicherheitsprobleme durch „optimierende“ Compiler umfassen nicht nur klassische Bugs, sondern auch Timing-Leaks, bei denen geheime Informationen über die Ausführungszeit austreten
- Das EuroS&P-2018-Paper von Laurent Simon, David Chisnall und Ross Anderson warnt, dass Compiler-Upgrades ohne Vorwarnung Timing-Kanäle in zuvor sicheren Code öffnen können
- Das im Paper von 2018 hervorgehobene Beispiel war Code, der mit
bool einen von zwei Werten auswählt; bool veranlasst den Compiler, bedingte Sprünge zu erzeugen
- In Krypto-Implementierungen ist es üblich, dies zu vermeiden, indem
bool aus kritischem Code entfernt und separate Vergleichsfunktionen mit konstanter Laufzeit geschrieben werden
- OpenSSL wird mit 37 Funktionen zitiert, die dafür deklariert werden
- Der Fall
curve25519-donna und MSVC 2015 aus dem Jahr 2015 wird im Text als Missverständnis eingeordnet
- Tatsächlich wurden
int64-Operationen beim Kompilieren für 32-Bit-x86 in Aufrufe von Microsofts 32-Bit-int64-Bibliothek llmul.asm übersetzt
- Das Timing-Leak entstand durch datenabhängige Verzweigungen in
llmul.asm, und diese Bibliothek sollte ebenfalls unter den vernünftigen Begriff von Quellcode fallen
- Im Juni 2024 bestätigte Antoon Purnal, dass der Kyber-Referenzcode unter Clang 15 und neuer bei bestimmten Optimierungsoptionen Timing-Angriffe ermöglichen kann
- Die problematische Form war
(-((x>>j)&1))&y; diese Berechnung ergibt y, wenn das j-te Bit von x gesetzt ist, andernfalls 0
- Clang wandelt das entsprechende Bit per Bit-Test-Befehl in ein
bool um und erzeugt anschließend eine bedingte Verzweigung auf Basis dieses bool
- Intern in LLVM verarbeitet
combineShiftAnd1ToBitTest in lib/CodeGen/SelectionDAG/DAGCombiner.cpp diese „Optimierung“
- Diese Funktion wurde im September 2019 von Sanjay Patel hinzugefügt und später von mehreren Personen geändert
- Auch bei GCC gibt es ähnliche Grenzüberschreitungen
- Ein GCC-Patch von ARM aus dem November 2021 wandelt
(-x)>>31 in -(x>0) um
- Im April 2024 wurde davor gewarnt
TIMECOP und Prüfung auf konstante Laufzeit
- TIMECOP 2 ist in das Kryptotest-Framework SUPERCOP eingebettet und prüft automatisch von geheimen Werten abgeleitete bedingte Verzweigungen in kompiliertem Code, der als konstantzeitlich deklariert ist
- Geprüft werden neben bedingten Verzweigungen auch Array-Indizes, die aus geheimen Werten abgeleitet sind
- Das KyberSlash-Paper beschreibt außerdem einen Patch, der aus geheimen Werten abgeleitete Divisionen prüft
- TIMECOP 1 war ein Werkzeug, das Moritz Neikes durch Änderungen an SUPERCOP erstellt hatte und das den Ansatz von Adam Langleys ctgrind automatisierte
- TIMECOP 2 erweitert den bisherigen Ansatz in mehreren Punkten
- RNG-Ausgaben werden automatisch als geheime Werte markiert
- „Declassification“ wird unterstützt
- Die Angabe von „public inputs“ wird unterstützt
- Es läuft auf mehreren Cores
- TIMECOP hat klare Grenzen
- Es kann nur Befehle behandeln, die Valgrind unterstützt, und bricht daher etwa bei AMD-XOP-Befehlen ab
- Es prüft nur Datenflüsse, die in tatsächlichen Testläufen sichtbar werden
- Die Arbeit an Werkzeugen zur Prüfung von Constant-Time-Verhalten geht weiter; eine Liste einschlägiger Tools findet sich unter ct-tools
- Eine TIMECOP-entsprechende Prüfung wurde in die Testsuite von libmceliece aufgenommen und könnte sich auch auf andere Bibliotheken ausbreiten
Methoden zum Umschreiben in konstante Laufzeit
- Nachdem Codefragmente mit variabler Laufzeit gefunden wurden, braucht es eine Methode, sie ohne Bugs in konstante Laufzeit umzuschreiben
- In einem Vortrag im Juli 2024 wurden einige Constant-Time-Funktionen vorgestellt, die libmceliece und SUPERCOP bereitstellen
- Die Dateinamen lauten
crypto_{int,uint}{8,16,32,64}.h
- Diese Dateien können in andere Projekte kopiert und dort verwendet werden
- Die Beispielfunktion
crypto_uint32_bitmod_mask(x,j) hat dieselbe Wirkung wie -((x>>(j&31))&1), verhindert aber, dass der Compiler das 1-Bit-Ergebnis sieht
- Als komplexeres Beispiel gibt es auch
crypto_uint32_max(x,y)
- Das Paper von 2018 behandelt einen Tweak, der Clang/LLVM um die Constant-Time-Funktion
__builtin_ct_choose(bool cond, x, y) ergänzt
- Das Paper behauptet fälschlich, dass diese eine Funktion ausreichend sei
- Diese Funktion könnte irgendwann in Compiler aufgenommen werden, aber es kann lange dauern, bis Projekte sich darauf verlassen können
- Die Implementierungsweise wird als fragiler eingeschätzt als
crypto_{int,uint}{8,16,32,64}.h
Probleme im Voraus vermeiden
- Wenn Tests vor der Auslieferung kompilierter Bibliotheken vom Compiler eingeführte Timing-Leaks finden, kann während der Code umgeschrieben wird für die Auslieferung die vorherige Compiler-Version verwendet werden
- Dieser Ansatz ist eine vorübergehende Gegenmaßnahme, die Nutzer weiterhin schützt
- Eine Lösung besteht darin, Bibliotheken als Assembly auszuliefern
- Der RWC-2024-Vortrag Adoption of high-assurance and highly performant cryptographic algorithms at AWS stellt schnelle X25519-Software vor, für die bewiesen ist, dass sie X25519 für alle Eingaben korrekt berechnet
- Die Implementierung ist in Assembly in zwei Versionen für 64-Bit-Intel/AMD-CPUs und zwei Versionen für 64-Bit-ARM-CPUs geschrieben
- Die Korrektheitsaussage ist ein Theorem über den Maschinencode, den Nutzer tatsächlich ausführen, und der Beweis wird mit dem Theorembeweiser HOL Light verifiziert
- Bei Kryptosoftware, die dieses Niveau nicht erreicht, bleibt allerdings weiterhin das Problem der schwierigen Auditierbarkeit von Assembly
- Für in C, C++ und ähnlichen Sprachen geschriebenen Code werden auch schnelle Wege untersucht, eine „Impfung“ gegen Timing-Leaks einzubauen
Experiment mit dem clang-vs-clang-Patch
x&1 und x>>31 haben gemeinsam, dass es nur zwei mögliche Ergebnisse gibt
x&1 ist 0 oder 1
x>>31 bei uint32 ist 0 oder 1
x>>31 bei int32 ist 0 oder -1
- Solche Formen machen es Autoren von Compiler-„Optimierungen“ leicht, ein 1-Bit-Ergebnis in
bool zu stecken
- Es gibt die Empfehlung, immer mit
-fwrapv zu kompilieren, damit GCC und Clang Zweierkomplement-Arithmetik annehmen
- Schon ein einfacher Scan des Quellcodes nach
&1, 1&, >>31 usw. liefert viele Beispiele, doch hier wurde anders gescannt, indem direkt ein Patch in den LLVM-„optimizer“ eingefügt wurde
- Der Patch beginnt bei LLVM-Commit
68df06a0b2998765cb0a41353fcf0919bbf57ddb, sucht nach &1 und >>31 und gibt folgende Warnung aus
please take this away before clang does something bad
- Ein Beispiel für den Kompilierbefehl ist
clang -Rpass-analysis=clang-vs-clang -O -c x.c
- Die Testfunktion lautet wie folgt
int sra31(int x)
{
x >>= 31;
return x;
}
- Dass dieselbe Warnung mehrfach erscheint, ist nicht überraschend
- Der Compiler versucht immer weiter, „Optimierungen“ anzuwenden, bis kein Fortschritt mehr erzielt wird
- Die Ausgabe von
clang-vs-clang unterscheidet bei Shifts zwischen signed und unsigned
- Dieser Unterschied ist für manuelles oder automatisches Umschreiben auf Basis von
crypto_{int,uint}{8,16,32,64}.h wichtig
- Als eine Möglichkeit zur Automatisierung von Source-Transformationen wird
clang-tidy genannt
- Code, der durch
#ifdef ausgeschlossen ist oder vor dieser „Optimierungs“-Phase entfernt wurde, erzeugt keine clang-vs-clang-Warnung
SUPERCOP-Ausführungsergebnisse und gefundene Fälle
- SUPERCOP 20240716 wurde auf einem Dual-EPYC 7742 mit
./data-do-biglittle ausgeführt
- Overclocking wurde deaktiviert
- Die SUPERCOP-Compilerliste wurde so angepasst, dass
clang-vs-clang verwendet wird, indem den clang-Zeilen in okcompilers/{c,cpp} -Rpass-analysis=clang-vs-clang hinzugefügt wurde
- Die Ergebnisse lagen nach 3 Stunden vor
- Die Clang-Ausgabe umfasste insgesamt 675.752 Zeilen
- Die Originalgröße betrug 210.786.494 Byte
- Das komprimierte Ergebnis ist
20240803-fromclang.txt.gz mit 3.595.199 Byte
- Die Ausgabe enthält viel Rauschen, das dadurch entsteht, dass Quellcode-Verzweigungen auf Basis öffentlicher Daten intern in Clang
&1 erzeugen
- Ein eindeutig vorab änderbares Beispiel ist folgendes
a0 += (a0>>15)&106;
- Ein Beispiel, das sich per einfachem Source-Scan nur mit C-Parsing-Aufwand finden ließe, ist folgendes
- Das Makro
ONE8 ist als ((uint8_t)1) definiert
*pk2^=(((* pk_cp)>>ir)&ONE8)<<jr;
- Ein noch schwieriger zu findendes Beispiel stammt aus einem AVX2-Intrinsic-basierten Makro
signmask_x16(x) ist als _mm256_srai_epi16((x),15) definiert
- Dies verschiebt jedes signed 16-Bit-Stück innerhalb eines 256-Bit-Vektors um 15 Bit nach rechts
mask = signmask_x16(sub_x16(x,const_x16((q+1)/2)));
- Dieser AVX2-Fall hat keine hohe Priorität
- Damit eine Vektoroperation in eine bedingte Verzweigung umgewandelt wird, müsste mit AVX-512 kompiliert werden, und der Compiler müsste die merkwürdige Entscheidung treffen, ein vektorisiertes
bool in serielle bedingte bool-Verzweigungen umzuwandeln
- TIMECOP verwendet Valgrind, und Valgrind unterstützt AVX-512 nicht
- Derzeit wird nicht empfohlen, für AVX-512 zu kompilieren
int128 und eine breitere Richtung für Gegenmaßnahmen
- Der interessanteste Fund war ein Fall, in dem ein 64-Bit-Rechtsshift von
int128 eine >>-Warnung auslöste
- Eine
int128-Implementierung kann intern einen 63-Bit-Rechtsshift verwenden, um das Vorzeichen des oberen 64-Bit-Worts zu ermitteln
- Wenn Clang wie GCC Unterstützung dafür hinzufügt, einen 63-Bit-Rechtsshift in
bool und anschließend in eine bedingte Verzweigung umzuwandeln, könnte viel int128-Code plötzlich variable Laufzeit haben
- In diesem Fall wäre die Lage ähnlich wie im Titel des Papers von 2015 behauptet, nur dass es diesmal tatsächlich auch ohne
bool im Quellcode passiert
- Der einfachste Schutz auf Source-Ebene besteht darin, die bestehende
int128-Implementierung des Compilers zu vermeiden und crypto_int128-Funktionen zu verwenden
- Anders als
int128 von GCC und Clang kann crypto_int128 auch auf kleinen 32-Bit-Plattformen funktionieren
- Die Idee, geheime Datentypen in GCC und Clang zu ergänzen, wirkt gut, doch angesichts der Struktur beider Compiler ist kein klarer Weg erkennbar, sie robust zu machen
- Mehr Hoffnung wird in Compiler gesetzt, die von Anfang an für Sicherheit entworfen wurden
- Zu den sicherheitsorientierten Compilern, die eine neue Eingabesprache verlangen, gehören FaCT und das aktiv entwickelte Jasmin
- Zwar gibt es Bedenken wegen des Aufwands für Code-Umschreibungen, doch angesichts der Art, wie aktuelle Compiler bestehenden Code behandeln, sind in irgendeiner Form Maßnahmen nötig
1 Kommentare
Meinungen auf Hacker News
Code, der undefiniertes Verhalten auslöst, als Compiler-Bug zu bezeichnen, nur weil er nicht wie gewünscht funktioniert, ist nicht richtig.
Das ist ungefähr so, als würde man
ddmit falschen Argumenten ausführen, dabei Daten löschen und dann behaupten,ddhabe einen Bug.Es ist schwer, das als Bug im Quellcode oder im Compiler zu betrachten; treffender ist, dass der C-Standard nach den Maßstäben des Autors zu wenig spezifiziert ist und dadurch auf manchen Targets Sicherheitsbugs erzeugt.
Letztlich können die Autoren des C-Standards nicht auch noch das Verhalten der Hardware definieren, sondern nur die Sprachsemantik; daher wird die Kryptografie zwangsläufig unter Bugs leiden, die der Hardware anzulasten sind.
Einer der Vorteile von Rust ist, dass potenziell undefiniertes Verhalten auf
unsafe-Blöcke beschränkt wird. Trotzdem hat Rust zwar vieles definiert, was in C undefiniertes Verhalten wäre, aber sobald man inunsafe-Code geht, ist es sehr leicht, versehentlich in subtile Fälle von undefiniertem Verhalten zu geraten.Ein drittes Modell, das still fehlschlägt und unvorhersehbaren Code erzeugt, ist nur für Compiler-Autoren nützlich. Sich hinter der Spezifikation zu verstecken, bringt echten Nutzern keinen Vorteil.
Diese Optimierungen machen Code kaputt, der zuvor gut funktioniert hat. Compiler-Autoren könnten Abwärtskompatibilität priorisieren, tun es aber nicht.
Außerdem verbessern solche Optimierungen die Performance realer Programme nicht in nennenswertem Maße; man müsste also das Argument entkräften, dass dieser Trade-off, Code kaputtzumachen, es nicht wert ist.
Ich mag Bernstein, aber manchmal verrennt er sich und wird radikal; dieser Artikel ist ein gutes Beispiel dafür. Am Ende des Textes räumt er das selbst halb ein.
Ein großer Teil des Artikels dreht sich um den Nebenaspekt, wie groß die Optimierungsgewinne sind; selbst mit Daten hängt diese Bewertung vom jeweiligen Use Case ab.
Die zentrale Beschwerde ist, dass C-Compiler Semantik nicht berücksichtigen, die sich in der Sprache nicht ausdrücken lässt — was nicht überraschend ist.
Am Ende sagt er: „Verwende eine Sprache, in der du die benötigte Semantik ausdrücken kannst.“ Der ganze Artikel hätte durch diesen einen Satz ersetzt werden können.
Bei einem beträchtlichen Teil davon ist die Begründung fragwürdig, und es macht das Schreiben korrekter Programme schwieriger.
C und C++ sind ungeeignet, um Algorithmen mit Constant-Time-Garantien zu schreiben.
Im Standard gibt es kaum ein Konzept von Echtzeit, und auch Compiler bieten keine zusätzlichen Garantien als Erweiterungen.
Aber den Compiler-Entwicklern dafür die Schuld zu geben, geht in die falsche Richtung.
Auf Intel-CPUs kann
clangoder irgendetwas anderes im User Mode keinen korrekten Code erzeugen. Denn korrekter Code existiert von vornherein nicht.https://www.intel.com/content/www/us/en/developer/articles/t...
Wenn man sich
DOITMin dem Dokument ansieht, ist es für eine Kryptobibliothek im Userspace schlicht unmöglich, das benötigte Bit zu setzen.Sobald er eingeschaltet ist, funktioniert er auch im Userspace gut; möglich wäre also etwa ein prozessweites Flag, das über den Systemaufruf
prctlaktiviert wird, wobei der Scheduler beim Task-Wechsel dasMSRanpasst.Schon der Satz „Wann immer möglich, weigern sich Compiler-Autoren, Verantwortung für die von ihnen erzeugten Bugs zu übernehmen“ zeigt, wie selten ein Blogbeitrag fachlich so schnell auseinanderfällt.
Wenn man sogar dem Link folgt, geht es nur um ganz grundlegendes C-Wissen: Undefined Behavior bedeutet nicht, dass ein „beliebiger Wert“ entsteht.
Auch bei Undefined Behavior ist der Quellcode zwar fehlerhaft, das erzeugte Programm aber oft weiterhin korrekt. Wenn ein Compiler-Autor später eine neue Optimierung einbaut und auf Grundlage dieses Undefined Behavior ein fehlerhaftes Programm erzeugt, beginnt die Schuldzuweisung.
Was man ungern anerkennt: Die Verantwortung gegenüber den Nutzern verteilt sich auf alle Seiten. Wenn bei einer CRUD-App wegen einer
NULL-Dereferenzierung der Akku in Brand gerät, würde kein vernünftiger Mensch nur dem App-Entwickler vorwerfen, einenNULL-Check vergessen zu haben.Auch Compiler-, Betriebssystem- und Hardwarehersteller müssen Verantwortung für verantwortungslos konzipierte Produkte übernehmen; mit dem ISO-Standardbegriff „Undefined Behavior“ ist die Sache nicht erledigt. Alle Mitglieder der Lieferkette teilen die Verantwortung, vorherzusehen, wie ein Produkt missbraucht werden kann, und damit angemessen umzugehen.
Undefined Behavior existiert, um Wert zu schaffen. Man kann Sprachen auch ohne so etwas bauen; der Grund, warum es trotzdem existiert, sind Portabilität und die Flexibilität für Compiler-Autoren.
Der Kern des Beitrags ist die Frage, ob diese Flexibilität im Vergleich zur Schwierigkeit, Programme ohne Undefined Behavior zu schreiben, ihren Wert hat.
Der Autor ist der Ansicht, dass das durch Bugs verlorene Geld größer wirkt als das durch schnelleren Bytecode eingesparte Geld, und dass der Einfluss der Compiler-Autoren bei der Festlegung von Sprachstandards groß ist, weshalb der Wille zur Korrektur gering ist.
Nebenbei:
clanghat das Attributclang::optnone, mit dem sich alle Optimierungen pro Funktion abschalten lassen, und GCC hat das hervorragende Attributgnu::optimize, mit dem man Optimierungen per Name hinzufügen oder entfernen oder unabhängig von Compiler-Flags ein Optimierungslevel festlegen kann.gnu::optimize(0)ähnelt diesemclang-Flag. Inclanggibt es außerdemclang::no_builtins, das speziell Optimierungen fürmemcpyundmemsetabschaltet.optimizesollte nur zu Debugging-Zwecken verwendet werden und ist nicht für Produktionscode geeignet.“https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attribute...
Für die Ziele, die Leute aus der Kryptografie wollen, etwa Constant-Time Evaluation und das Verbergen geheimer Werte, habe ich durchaus Verständnis.
Aber ein Allzweck-Compiler denkt die meiste Zeit nicht an solche Dinge, daher dürfte es kaum mehr sein als ein Hack, der meistens funktioniert.
Wenn man es ernst meint, braucht man wohl einen eigenen Spezial-Compiler oder muss weiter auf Assembly setzen.
Irgendwann werden wir die heutige Zeit wohl als die schlechten alten Zeiten betrachten und uns von C wegbewegt haben, hin zu Sprachen mit deutlich weniger undefiniertem Verhalten
In C ist es viel zu einfach, Ausdrücke zu schreiben, die zwar kompilieren, bei denen der Compiler aber unmöglich erkennen kann, was beabsichtigt war
In Python kann man zum Beispiel Code wie
result = [something(value) for value in set_object]schreiben. Da einset-Objekt ungeordnet ist, ist klar, dass die Reihenfolge der Verarbeitung der Elemente und die Reihenfolge des Ergebnisses nicht wichtig sind; das eröffnet auf Sprachebene viele Optimierungen, ohne dass der Compiler die Absicht des Autors erraten mussÄhnlicher Code in anderen Sprachen mit unveränderlichen Daten geht noch einen Schritt weiter: Da
something(value1)something(value2)nicht beeinflussen kann, lässt sich das parallel ausführen, ob in Threads oder ProzessenEin großer Teil der Optimierung von C-Compilern besteht darin, sich Code-Muster anzusehen und Wege zu finden, das, was der Autor vermutlich gemeint hat, schneller zu machen. C kann Absichten im Vergleich zu modernen Sprachen nur unzureichend ausdrücken, sodass es zwar Spielraum für Vermutungen gibt, aber für ordentliche Performance muss man solche Schlüsse ziehen
Trotzdem könnte es ein verkappter Segen sein, ähnlich wie die Sache, dass das Hubble-Teleskop eine Brille brauchte. Um Einschränkungen zu überwinden, wurden hervorragende Techniken entwickelt, und nachdem das Problem behoben war, lieferten diese Techniken eine viel höhere Performance als ursprünglich erwartet. Wenn man Optimierungen von C-Compilern auf Nicht-C-Sprachen anwendet, könnten sie wie Superkräfte wirken
Im Grunde ähnelt das undefiniertem Verhalten, nur dass es sich nicht unmittelbar als Sicherheitsproblem zeigt, sondern als falsches Ergebnis. Natürlich kann ein falsches Ergebnis später zu einem Sicherheitsproblem führen
Anders als bei undefiniertem Verhalten ist es praktisch unmöglich, einen „Sanitizer“ zu bauen, der überprüft, ob der Code für alle möglichen
set-Reihenfolgen funktioniertgccundclanghaben viele Low-Level-Hinweise, die es in anderen Sprachen oft nicht gibt. Dazu gehören__builtin_expect/__builtin_unpredictable,__builtin_unreachable/__builtin_assume,#pragma clang loop vectorize(assume_safety)/#pragma GCC ivdepsowie Pragmas zum Abschalten von Loop-Unrolling oder Vektorisierung oder zum Auswählen bestimmter WerteWas meiner Ansicht nach am deutlichsten fehlt, ist eine Optimierungsbarriere, die explizit verhindert, dass der Compiler aus der Herkunft eines Werts Schlüsse zieht. Mit
__asm__geht das bis zu einem gewissen Grad, hat aber unerwünschte Nebenwirkungen und erfordert plattformspezifische Namen für RegistertypenEs gibt eindeutig auch Potenzial für High-Level-Optimierungen auf Basis von Absichten. Denkbar wäre etwa, vor
n-maligempushin einer Schleife Platz in einer Array-Liste zu reservieren, Hashmap-Lookups wiecontains→get→putmit demselben Schlüssel zusammenzufassen oder globale Allokationsvorgänge lokal herzuleiten, um Objekte und Allokationen zu eliminierenC ist nah genug an realer Hardware, dass Programmierer einfach sagen können, was passieren soll; der Compiler muss die Absicht der Programmierer daher nicht erraten
Sprachen, die solche Speicheroptimierungen implementieren, sind meist Java-artig und haben von Anfang an eine aggressive vorauseilende Pessimierung, wodurch überhaupt erst der Anreiz für solche Optimierungen entsteht. Aber selbst mit diesen Optimierungen wird der Verlust nicht wettgemacht
Der Punkt ist: C ist auch nicht toll, aber die anderen sind schlechter
Wenn einem die Semantik von C nicht gefällt, sollte man nicht auf Compiler-Ingenieure wütend sein, sondern eine andere Programmiersprache verwenden
qhasmertragen kann. Nicht einmal Zig. Diese Einschätzung von ihm ist daher nicht besonders überraschendEin erfrischender Text, der eine Perspektive vermittelt, die man nicht oft hört. Ebenfalls lesenswert: https://gavinhoward.com/2023/08/the-scourge-of-00ub/