- Im master-Branch von Zig wurden Verbesserungen bei der Verarbeitung nicht ABI-großer Integer im LLVM-Backend sowie eine neue
@bitCast-Semantik zusammengeführt, um sowohl Optimierungsprobleme als auch Inkonsistenzen im Sprachverhalten zu bereinigen - Integer mit beliebiger Bitbreite wie
u4,i13undu40werden in SSA-Werten weiterhin als bit-int behandelt, beim Speichern im Speicher jedoch auf Integer in ABI-Größe erweitert - Das bisherige
@bitCastkam einer Neuinterpretation von Speicherbytes nahe, die neue Definition interpretiert dagegen anhand des logischen Bit-Arrays eines Typs und reduziert damit die Endian-Abhängigkeit - Die Änderungen wurden auf das LLVM- und C-Backend sowie auf die
comptime-Ausführung ausgeweitet; auch die entsprechenden Verwendungen in Standardbibliothek, Compiler undcompiler_rtwurden mit überprüft - Durch wiederhergestellte, zuvor ausbleibende LLVM-Optimierungen wurde bereits im Zig-Compiler selbst eine Leistungssteigerung von etwa 5 % beobachtet; in 0.17.0 sind daher in manchen Fällen auch Laufzeitverbesserungen zu erwarten
Änderung bei der Verarbeitung von Integern mit beliebiger Bitbreite im LLVM-Backend
- Zig hat Integer-Typen mit beliebiger Bitbreite wie
u4,i13undu40bislang direkt auf die bit-int-Typeni4,i13undi40in LLVM IR abgesenkt - Dieser Ansatz führte dazu, dass die Semantik der Speicherdarstellung in LLVM dem Optimierer unnötige Einschränkungen auferlegte; zudem erzeugt Clang solches LLVM IR nicht, weshalb diese internen LLVM-Pfade nicht ausreichend getestet waren
- In den letzten Jahren wurden tatsächlich Fälle von fehlenden Optimierungen und Fehlkompilierungen beobachtet
- Der neue Ansatz behält für die Manipulation von SSA-Werten bit-int-Typen bei, erweitert beim Speichern in den Speicher jedoch per zero-extend oder sign-extend auf Typen in ABI-Größe wie
i8,i16undi32 - Dieses Lowering entspricht der Art, wie Clang C-
_BitInt(N)absenkt, und dürfte damit einen in LLVM besser unterstützten Pfad nutzen
Grenzen des bisherigen @bitCast
- Das bisherige
@bitCastentsprach konzeptionell eher dem folgenden Ablauf- Einen Pointer auf den Operandenwert holen
- Diesen Pointer in einen Pointer auf den Zieltyp umwandeln
- Den Wert über diesen Pointer laden
- Die bisherige Definition kam also eher einer Neuinterpretation von Bytes im Speicher gleich als der logischen Struktur eines Typs
- Mit der Zeit wich das tatsächliche Verhalten von dieser Definition ab, und obwohl
@sizeOf(u24)auf den meisten Targets größer ist als@sizeOf([3]u8), war@bitCastvon[3]u8nachu24dennoch erlaubt - Das LLVM-Backend implementierte eine nicht ausreichend spezifizierte
@bitCast-Semantik; nachdem die Art geändert wurde, wie Integer-Typen im Speicher abgelegt werden, traten in der Compiler-Testsuite Illegal Behavior und Abstürze auf - Statt dem LLVM-Backend zusätzliche Logik hinzuzufügen, um das bisherige Verhalten nachzuahmen, fiel die Entscheidung, die neue
@bitCast-Definition durchgängig zu implementieren
Neue @bitCast-Semantik
- Die neue Semantik basiert auf dem 2024 eingereichten und angenommenen Sprachvorschlag #19755
- Diese Semantik war bereits im self-hosted x86_64-Backend implementiert und wurde mit dieser Änderung auf das LLVM- und C-Backend sowie auf die
comptime-Ausführung ausgeweitet - Das neue
@bitCastarbeitet nicht mit Speicherbytes, sondern mit der logischen Reihenfolge der Bits, die einen Typ darstellenu5besteht aus 5 logischen Bits vom least-significant bit bis zum most-significant bit[2]u5besteht aus 10 logischen Bits, bei denen auf die 5 Bits des ersten Elements die 5 Bits des zweiten Elements folgen
- Bei einfachen Umwandlungen zwischen Integern, etwa von
u8zui8gleicher Größe, bleiben die Bits unverändert und das höchstwertige Bit wird als Vorzeichenbit interpretiert - Die
@bitCast-Semantik zwischen Integer-Typen undpacked structoderpacked unionbleibt ebenfalls erhalten
Geändertes Verhalten bei Arrays und Vektoren
- Die neue Semantik unterscheidet sich vom bisherigen Verhalten dort, wo Aggregate-Typen wie Arrays und Vektoren beteiligt sind
- Wird zum Beispiel
[2]u8per@bitCastinu16umgewandelt, hing das Ergebnis in der bisherigen Semantik vom Endian des Targets ab- Auf big-endian-Targets wurde das erste Array-Element zu den oberen 8 Bit
- Auf little-endian-Targets wurde das erste Array-Element zu den unteren 8 Bit
- Die neue Semantik betrachtet nur die logische Bit-Darstellung, ist daher endian-unabhängig, und auf allen Targets wird das erste Array-Element zu den unteren 8 Bit
- Im Allgemeinen liegt das Verhalten damit näher am bisherigen Verhalten auf little-endian-Targets
- Auch untypische Umwandlungen wie von
[2]u3nach@Vector(3, u2)sind möglich- Die logischen Bits des Arrays werden aneinandergehängt und anschließend in 2-Bit-Einheiten gelesen, um die Vektorelemente zu bilden
- Das lässt sich auch nutzen, um einen Integer per
@bitCastin@Vector(n, u1)zu zerlegen und so einen Vektor einzelner Bits zu erhalten
Mit umgesetzte Vorschläge und Migration
- Im Zuge dieser Arbeit wurden auch kleinere angenommene Vorschläge rund um
@bitCastumgesetzt - Da sich die neue Semantik inhaltlich deutlich von der bisherigen unterscheidet, wurden die
@bitCast-Verwendungen in unterstützenden Bibliotheken wie Standardbibliothek, Compiler undcompiler_rtüberprüft - Der zugehörige PR ist codeberg.org/ziglang/zig/pulls/35711; mit dem Merge in master wurden auch mehrere Issues geschlossen
- Die geänderte Semantik und das empfohlene Migrationsverfahren sollen in den Release Notes zu Zig 0.17.0 dokumentiert werden
Zu erwartende Performance-Effekte in 0.17.0
- Die ursprünglich angestrebte Änderung beim Lowering nicht ABI-großer Integer im LLVM-Backend hat erfolgreich zuvor ausbleibende Optimierungen wieder aktiviert
- Das entsprechende Ergebnis lässt sich unter demonstrably successful nachvollziehen
- Obwohl der Zig-Compiler selbst intern nicht besonders viele Integer mit beliebiger Bitbreite verwendet, zeigt er dank besserer Optimierungen bereits eine Leistungssteigerung von etwa 5 %
- In 0.17.0 könnte es daher in einem Teil der Programme auch zu kleinen Laufzeitverbesserungen kommen
1 Kommentare
Lobste.rs-Meinungen
Im Artikel heißt es, die logische Bit-Darstellung sei endian-unabhängig, aber die eigentliche Erklärung wirkt wie ein klar Little-Endian-Ansatz, der weder Big-Endian-Bitreihenfolge noch Bytereihenfolge unterstützt
Laut einem neuen Entwicklungslog vom 25. Juni 2026 wurden die neue
@bitCast-Semantik und Verbesserungen am LLVM-Backend in einen kürzlichen Pull Request gemergtInteressant, aber ich frage mich, ob auf selten getesteten Big-Endian-Zielen Code wie der folgende plötzlich kaputtgehen könnte
In Nicht-Zig-Pseudocode wäre das:
In der Praxis dürfte das kein großes Problem sein; unter den tausenden von
@bitCastim Zig-Repository waren offenbar deutlich weniger als 100 von dieser Änderung betroffenEhrlich gesagt glaube ich auch nicht, dass die meisten Zig-Nutzer exakt wussten, wie
@bitCastbisher bei Umwandlungen zwischen Arrays/Vektoren und Skalaren funktioniert. Viel Code, der bislang nur auf dem System des Autors getestet wurde und deshalb nur auf Little Endian lief, dürfte jetzt stattdessen überall funktionierenAls früherer C-Programmierer erinnere ich mich, dass die Bitfelder in C nie besonders beliebt waren, weil ihr Verhalten zwischen Architekturen nicht portabel war
Die neue Zig-
@bitCast-Semantik ist eine portable abstrakte Semantik, die auch auf unterschiedlichen Architekturen zum gleichen Ergebnis führt, und genau das war wohl nötigIch arbeite gerade selbst am Design von Bitfeldern und Bitcasts in meiner Sprache und werde mir deshalb die Zig-Design- und Implementierungsdokumente genauer ansehen, um klarer zu definieren, wie mein Code sich verhalten soll
packed structundpacked union, und beide sind so definiert, dass sie gut zur neuen@bitCast-Definition passenBei
packed structwerden die Bits der Felder in eine „Basis-Ganzzahl“ gepackt. Wenn die Felder zum Beispielbool,u6undi9sind und die Basis-Ganzzahlu16ist, dann ist das niederwertigste Bit vonu16dasbool, die nächsten 6 Bits sindu6und die verbleibenden 9 Bits sindi9. Zigs packed struct ist also im Wesentlichen syntaktischer Zucker über mehreren Shifts und Maskenpacked unionhat ebenfalls eine Basis-Ganzzahl, aber alle Felder müssen genau dieselbe Bitbreite wie die Basis-Ganzzahl verwenden. Deshalb ist das Schreiben in ein Feld und Lesen aus einem anderen Feld fast identisch mit@bitCastunter der neuen Semantik. Allerdings dürfen Felder inpacked union/packed structkeine Array- oder Vektortypen habenMeiner Meinung nach eignen sich diese Werkzeuge sehr gut, um „bitbezogene Strukturen“ auszudrücken. Man kann mehrere Werte in ein
packed structbitpacken und es wie C-Bitfelder verwenden, und weil es syntaktischer Zucker über Bitoperationen ist, lassen sich auch Bit-Flags, die man in C mit typsicheren Makro-Konstruktionen erschlagen musste, sauber darstellenZum Beispiel können RWX-Zugriffsflags in C als
ACCESS_READ,ACCESS_WRITE,ACCESS_EXEC-Makros plus eineuint8_t-API vorliegen, während man in ZigAccess = packed struct(u8)mit den Feldernread,write,execundreserveddefinieren und in der API direktAccessannehmen kannMit
packed structundpacked unionlassen sich auch ziemlich ungewöhnliche Bit-Anordnungen ausdrücken. Im Symboltabellen-Eintrag des Mach-O-Objektformats gibt es aus historischen Gründen ein merkwürdigesn_type-Feld, das sich alspacked union(u8)mitbits: packed struct(u8)undstab: enum(u8)modellieren lässtBeim Arbeiten mit diesem
n_type-Wert braucht man keine manuellen Shifts oder Maskierungen. Man prüft einfachn_type.bits.is_stab != 0und macht dann, falls wahr, einswitchaufn_type.stab; andernfalls schaut man auf die anderen Felder vonn_type.bits. Umgekehrt kann man Werte auch als.{ .stab = .gsym }oder.{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }erzeugenDas ist zwar etwas länger geworden und geht über das Thema des ursprünglichen Artikels hinaus, aber wenn du nach Anregungen für ein neues Sprachdesign suchst, lohnt es sich, Zigs
packed structundpacked unionselbst auszuprobieren. Es sind einfache, aber ziemlich gute Werkzeuge