1 Punkte von GN⁺ 5 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • 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, i13 und u40 werden in SSA-Werten weiterhin als bit-int behandelt, beim Speichern im Speicher jedoch auf Integer in ABI-Größe erweitert
  • Das bisherige @bitCast kam 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 und compiler_rt wurden 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, i13 und u40 bislang direkt auf die bit-int-Typen i4, i13 und i40 in 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, i16 und i32
  • 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 @bitCast entsprach 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 @bitCast von [3]u8 nach u24 dennoch 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 @bitCast arbeitet nicht mit Speicherbytes, sondern mit der logischen Reihenfolge der Bits, die einen Typ darstellen
    • u5 besteht aus 5 logischen Bits vom least-significant bit bis zum most-significant bit
    • [2]u5 besteht 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 u8 zu i8 gleicher Größe, bleiben die Bits unverändert und das höchstwertige Bit wird als Vorzeichenbit interpretiert
  • Die @bitCast-Semantik zwischen Integer-Typen und packed struct oder packed union bleibt 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]u8 per @bitCast in u16 umgewandelt, 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]u3 nach @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 @bitCast in @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 @bitCast umgesetzt
    • @bitCast mit Pointer-Vektoren verboten: #18936
    • @bitCast für Enums erlaubt: ein Teil von #35602
  • Da sich die neue Semantik inhaltlich deutlich von der bisherigen unterscheidet, wurden die @bitCast-Verwendungen in unterstützenden Bibliotheken wie Standardbibliothek, Compiler und compiler_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

 
GN⁺ 5 시간 전
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

    • Endian-unabhängig scheint hier zu bedeuten, dass sich das Verhalten zwischen Little-Endian- und Big-Endian-Architekturen nicht unterscheidet
  • Laut einem neuen Entwicklungslog vom 25. Juni 2026 wurden die neue @bitCast-Semantik und Verbesserungen am LLVM-Backend in einen kürzlichen Pull Request gemergt

  • Interessant, 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:

    if target_is_little_endian {  
        my_int = @bitCast(my_array);  
    } else {  
        my_int = @bitCast([my_array[1], my_array[0]]);  
    }  
    
    • Das dachte ich auch, aber am Ende macht es das Problem nur größer, wenn man eine unvermeidliche Änderung aufschiebt
      In der Praxis dürfte das kein großes Problem sein; unter den tausenden von @bitCast im Zig-Repository waren offenbar deutlich weniger als 100 von dieser Änderung betroffen
      Ehrlich gesagt glaube ich auch nicht, dass die meisten Zig-Nutzer exakt wussten, wie @bitCast bisher 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 funktionieren
  • Als 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ötig
    Ich 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

    • Zigs wichtigste Alternative zu C-Bitfeldern sind vermutlich packed struct und packed union, und beide sind so definiert, dass sie gut zur neuen @bitCast-Definition passen
      Bei packed struct werden die Bits der Felder in eine „Basis-Ganzzahl“ gepackt. Wenn die Felder zum Beispiel bool, u6 und i9 sind und die Basis-Ganzzahl u16 ist, dann ist das niederwertigste Bit von u16 das bool, die nächsten 6 Bits sind u6 und die verbleibenden 9 Bits sind i9. Zigs packed struct ist also im Wesentlichen syntaktischer Zucker über mehreren Shifts und Masken
      packed union hat 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 @bitCast unter der neuen Semantik. Allerdings dürfen Felder in packed union/packed struct keine Array- oder Vektortypen haben
      Meiner Meinung nach eignen sich diese Werkzeuge sehr gut, um „bitbezogene Strukturen“ auszudrücken. Man kann mehrere Werte in ein packed struct bitpacken 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 darstellen
      Zum Beispiel können RWX-Zugriffsflags in C als ACCESS_READ, ACCESS_WRITE, ACCESS_EXEC-Makros plus eine uint8_t-API vorliegen, während man in Zig Access = packed struct(u8) mit den Feldern read, write, exec und reserved definieren und in der API direkt Access annehmen kann
      Mit packed struct und packed union lassen sich auch ziemlich ungewöhnliche Bit-Anordnungen ausdrücken. Im Symboltabellen-Eintrag des Mach-O-Objektformats gibt es aus historischen Gründen ein merkwürdiges n_type-Feld, das sich als packed union(u8) mit bits: packed struct(u8) und stab: enum(u8) modellieren lässt
      Beim Arbeiten mit diesem n_type-Wert braucht man keine manuellen Shifts oder Maskierungen. Man prüft einfach n_type.bits.is_stab != 0 und macht dann, falls wahr, ein switch auf n_type.stab; andernfalls schaut man auf die anderen Felder von n_type.bits. Umgekehrt kann man Werte auch als .{ .stab = .gsym } oder .{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } } erzeugen
      Das 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 struct und packed union selbst auszuprobieren. Es sind einfache, aber ziemlich gute Werkzeuge