1 Punkte von GN⁺ 2025-06-08 | 1 Kommentare | Auf WhatsApp teilen
  • Low-Level-Optimierung lässt sich in der Sprache Zig leicht umsetzen
  • Der Compiler führt Optimierungen in den meisten Situationen gut aus, aber manchmal muss die Absicht des Programmierers klar vermittelt werden, um bessere Leistung zu erzielen
  • Zig unterstützt mit Compile-Time-Ausführung (comptime) die Erzeugung hochperformanten Codes und starkes Metaprogramming
  • Im Vergleich zu Rust ermöglicht Zig durch Annotationen und eine explizite Codestruktur präzisere Optimierungen
  • Bei wiederholten Operationen wie String-Vergleichen kann mit comptime besserer Assembly-Code erzeugt werden als mit gewöhnlichen Funktionen

Optimierung und Zig

Wie die berühmte Warnung sagt: „Alles ist möglich, aber das Interessante bekommt man nicht leicht.“ Entsprechend gehört die Optimierung von Programmen immer zu den zentralen Interessen von Entwicklern. Für die Kosten von Cloud-Infrastrukturen, geringere Latenz und die Vereinfachung von Systemen ist Code-Optimierung unverzichtbar. Dieser Artikel erklärt vor allem das Konzept der Low-Level-Optimierung in Zig und die Stärken von Zig.

Kann man dem Compiler vertrauen?

  • Im Allgemeinen hört man oft den Rat „Vertrau dem Compiler“, in der Praxis kommt es jedoch vor, dass der Compiler anders als erwartet arbeitet oder sogar die Sprachspezifikation verletzt
  • Höhere Programmiersprachen erschweren es, Absicht (intent) klar zu vermitteln, was entsprechende Performance-Einschränkungen mit sich bringt
  • Low-Level-Sprachen können dem Compiler dank der Explizitheit des Codes die für Optimierungen nötigen Informationen liefern; vergleicht man etwa die Funktion maxArray in JavaScript und Zig, übermittelt Zig klare Informationen zu Typen, Alignment und möglichem Aliasing nicht erst zur Laufzeit, sondern bereits zur Compile-Time
  • Schreibt man dieselbe maxArray-Operation in Zig und Rust, erhält man fast identischen hochperformanten Assembly-Code, aber je besser sich die Absicht ausdrücken lässt, desto besser fällt das Optimierungsergebnis aus
  • Da man der Compiler-Leistung jedoch nicht immer vertrauen kann, sollte man in Engpässen Code und Compiler-Ergebnis direkt prüfen und nach Optimierungsmöglichkeiten suchen

Die Rolle von Zig

  • Dank Eigenschaften wie präziser Explizitheit, umfangreichen Builtins, Pointern und Annotationen, comptime sowie klar definiertem Illegal Behavior kann Zig optimierten Code ohne abstrakte Zusatzinformationen erzeugen
  • Rust garantiert durch sein Speichermodell grundsätzlich, dass Argumente kein Aliasing haben, während in Zig Annotationen wie noalias explizit nötig sind
  • Selbst wenn man nur LLVM IR als Maßstab nimmt, ist das Optimierungsniveau von Zig hoch
  • Vor allem ist Zigs comptime (Compile-Time-Ausführung) ein mächtiges Optimierungswerkzeug

Was ist comptime?

  • Zigs comptime wird für Codegenerierung, das Einbetten konstanter Werte und die Erzeugung typbasierter generischer Strukturen genutzt und spielt eine wichtige Rolle für bessere Laufzeitleistung
  • Mit comptime lässt sich Metaprogramming umsetzen
  • Anders als Makros in C/C++ oder das Macro-System von Rust ist comptime keine eigene Syntax, sondern normaler Code
  • comptime-Code verändert den AST nicht direkt, kann aber für alle Typen zur Compile-Time prüfen, anwenden und erzeugen
  • Die Flexibilität von comptime hat auch Verbesserungen in anderen Sprachen wie Rust beeinflusst und ist natürlich in die Sprache Zig integriert

Grenzen von comptime

  • Einige Macro-Funktionen wie Token-Pasting lassen sich durch Zigs comptime nicht ersetzen
  • Da Zig Lesbarkeit des Codes betont, sind das Erzeugen von Variablen außerhalb des Gültigkeitsbereichs oder Makrodefinitionen dieser Art nicht erlaubt
  • Stattdessen gibt es für Zigs comptime breite Metaprogramming-Anwendungsfälle wie Typ-Reflection, DSL-Implementierung und die Optimierung von String-Parsing

String-Vergleichsoptimierung mit comptime

  • Eine gewöhnliche String-Vergleichsfunktion lässt sich in jeder Sprache implementieren, aber wenn in Zig einer der beiden Strings als zur Compile-Time bekannte Konstante vorliegt, kann effizienterer Assembly-Code erzeugt werden
  • Ist zum Beispiel ein String immer "Hello!\n", kann statt auf Byte-Ebene in größeren Blöcken verglichen werden
  • Mit comptime lässt sich dafür hochperformanter Code zur Compile-Time erzeugen, etwa mit SIMD-Vektoren, Blockverarbeitung und Optimierungen für Restbytes
  • So lassen sich nicht nur wiederholte String-Vergleiche, sondern auch verschiedene mappings auf Basis statischer Daten, perfekte Hash-Tabellen, AST-Parser und andere performanceorientierte Implementierungen umsetzen

Fazit

  • Zig eignet sich sehr gut für Low-Level-Optimierung und ermöglicht dank expliziter Codestruktur und mächtigem comptime, Spitzenleistung direkt umzusetzen
  • Auch im Vergleich zu anderen Sprachen wie Rust bieten Zigs Compile-Time-Programmierung und Explizitheit große Vorteile bei der Entwicklung hochperformanter Software
  • Zigs Optimierungsfähigkeit wird auch künftig ein noch wichtigerer Wettbewerbsvorteil sein

1 Kommentare

 
GN⁺ 2025-06-08
Hacker-News-Kommentare
  • Was ich an Zig am interessantesten finde, sind die Einfachheit des Build-Systems, Cross-Compilation und das Streben nach hoher Iterationsgeschwindigkeit. Ich bin Spieleentwickler, daher ist Performance wichtig, aber für die meisten Anforderungen liefern die meisten Sprachen genug Performance. Deshalb ist das nicht das wichtigste Kriterium bei der Sprachwahl. Mit jeder Sprache kann man leistungsfähigen Code schreiben, aber ich ziele auf ein zukunftsfähiges Framework, das sich über Jahrzehnte warten lässt. C/C++ war die Standardwahl, weil es überall unterstützt wird, aber ich habe das Gefühl, dass Zig da gleichziehen kann
    • Ich habe aus Spaß versucht, Zig auf einem sehr alten Kindle-Gerät (Linux 4.1.15) laufen zu lassen, und war überrascht, wie ausgereift Zig ist. Das meiste funktionierte sofort, und sogar mit einem alten GDB konnte ich seltsame Bugs debuggen. Ich bin auch von Zig begeistert. Mehr zu den Erfahrungen gibt es hier
    • Ich habe das Gefühl, dass man mit den meisten Sprachen leistungsfähigen Code schreiben kann, aber ich möchte modularen Code, der auf Jahrzehnte ausgelegt ist. Ich mag Zig, aber ich denke, dass es bei langfristiger Wartbarkeit und Modularität Nachteile hat. Zig steht Kapselung feindlich gegenüber. Es ist nicht möglich, Struct-Member als private zu behandeln. Dieser Kommentar im Issue ist ein Beispiel. Die Position von Zig ist, dass es keine getrennte interne Repräsentation geben sollte und dass alle Nutzer die interne Implementierung kennen und diese dokumentiert/offengelegt werden sollte. Aber um API-Verträge, also den Kern modularer Software, zu wahren, muss man die interne Implementierung verbergen können, und das ist nicht möglich. Ich hoffe, dass Zig irgendwann private Felder unterstützt
    • Ich habe Rust nur leicht ausprobiert und es hat mir gefallen. Dann habe ich aufgehört, weil ich hörte, es sei „schlecht“, und benutze es jetzt wieder. Es gefällt mir immer noch. Ich verstehe nicht ganz, warum die Leute es so hassen. Die hässliche Generics-Syntax ist bei C# und TypeScript doch genauso. Auch der Borrow Checker ist leicht zu verstehen, wenn man Erfahrung mit Low-Level-Sprachen hat
    • Zig fühlt sich an wie einfacheres Rust und besseres Go. Andererseits bin ich von den auf Zig aufgebauten Tools, besonders bun, wirklich beeindruckt. bun hat mein Leben enorm vereinfacht. Das Rust-basierte uv bietet eine ähnliche Erfahrung
    • Ich stimme zu, dass C/C++ die Grundlage ist. Fast alles, was besser als C sein wollte, wurde am Ende doch wieder C++. Trotzdem darf man nicht aufhören, es zu versuchen. Rust und Zig sind der Beweis, dass man weiter auf etwas Besseres hoffen kann. Ich werde mich ab jetzt tiefer mit C++ beschäftigen
  • Selbst wenn moderne Compiler manchmal Sprachspezifikationen verletzen, ist Clangs Annahme über das Beenden unendlicher Schleifen laut Standard seit C11 korrekt. In C11 steht ausdrücklich: „Bei Schleifen, deren Steuerungsausdruck kein konstanter Ausdruck ist und die keine Ein-/Ausgabe-, volatile-, sync- oder atomic-Operationen ausführen, darf der Compiler annehmen, dass sie terminieren.“
    • In C++ gilt diese Regel (bis vor C++26) für alle Schleifen, aber wie gesagt in C nur für „Schleifen, deren Steuerungsausdruck kein konstanter Ausdruck ist“. Das heißt, eine offensichtliche Endlosschleife wie for(;;); muss tatsächlich eine Endlosschleife sein, und loop {} in Rust ebenso. Aber LLVM-Entwickler verhalten sich manchmal so, als würden sie nur C++-Compiler bauen, und wenn Rust sagt „bitte eine Endlosschleife“, wendet LLVM Optimierungen an nach dem Motto „nach C++-Maßstäben kann das nicht passieren“. Dadurch entstehen Probleme. Es wird also eine falsche Optimierung auf die falsche Sprache angewendet
  • Auch ohne comptime kann man String-Vergleiche in C problemlos inlinen und unrollen. Hier ein Beispiel
    • Der Einwand ist richtig! Das erste Beispiel war zu simpel. Ein besseres Beispiel ist der Compile-Time Suffix Automaton. Außerdem zeigt der oben verlinkte Godbolt-Code eher eines von zwei Dingen, die man gerade nicht tun sollte
  • Ich halte das angeführte Beispiel mit JavaScript-Code und angeblich ineffizientem von V8 erzeugtem Bytecode nicht für einen guten Vergleich. Für Zig und Rust wird verlangt, mit sehr aktueller Zielumgebung zu kompilieren, bei V8 werden solche Optimierungsoptionen aber nicht erzwungen. Tatsächlich können moderne JITs ebenfalls vektorisieren, wenn die Umstände es erlauben. Und die meisten modernen Sprachen behandeln String-bezogene Optimierungen ähnlich. Zur Referenz gibt es auch ein C++-Beispiel
    • JS und Zig zu vergleichen ist im Grunde wie Äpfel mit Obstsalat zu vergleichen. Das Zig-Beispiel verwendet Arrays mit festem Typ und fester Größe, während JS „generic“ Code ist, in den zur Laufzeit verschiedene Typen gelangen können. Deshalb kann ein JIT in JS viel schnellere Schleifen erzeugen, wenn man nur ausreichend Typinformationen liefert, auch wenn es nicht bis zur Vektorisierung geht. In der Praxis nutzt man TypedArray nicht so oft, weil die Initialisierung teuer ist und es sich erst bei häufiger Wiederverwendung lohnt. Im Artikel hieß es außerdem, der JS-Code sei aufgebläht, aber ein großer Teil davon sind Guards, weil der JIT den Array-Längencheck nicht blind vertrauen kann. Tatsächlich schreibt praktisch jeder Schleifen wie i < x.length, wodurch JIT-Optimierung greift. Insofern ist es ein wenig Nörgelei, wenn auch nur ein kleiner Unterschied
    • Bei den Godbolt-Beispielen für Rust und Zig kann man das Target auch auf ältere CPUs umstellen. Über die Beschränkung auf der JS-Seite hatte ich nicht nachgedacht. Und das C++-Beispiel zeigt, wie guten Code Clang erzeugt. Trotzdem ist das Assembly selbst momentan nicht besonders zufriedenstellend, selbst wenn man berücksichtigt, dass Zig für ein bestimmtes CPU-Target gebaut wird. Ein C++-Port des Compile-Time Suffix Automaton wäre wirklich interessant. Das ist ein realer Anwendungsfall für comptime, den C++-Compiler nicht erraten können
  • Ich zweifle an der Aussage, „High-Level-Sprachen fehlt die ‚intent‘, die Low-Level-Sprachen haben“. Ich würde eher sagen, dass es gerade die Stärke von High-Level-Sprachen ist, Absichten auf vielfältigere und detailliertere Weise auszudrücken
    • Ich stimme auch zu. Im Kern besteht der Unterschied zwischen High-Level- und Low-Level-Sprachen darin, dass man in High-Level-Sprachen Absicht ausdrückt, während man in Low-Level-Sprachen die Implementierungsmechanik selbst offenlegen muss
    • Mit „Absicht“ ist hier nicht geschäftliche Absicht gemeint wie „die Steuer für diesen Kauf berechnen“, sondern eher etwas in Richtung „dieses Byte um drei Stellen nach links schieben“, also was man den Computer konkret tun lässt. Zum Beispiel ist purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?; voller Absicht, aber wie daraus tatsächlich Maschinencode wird, ist nicht vorhersagbar
  • Das Allocator-Modell von Zig gefällt mir sehr. Es wäre schön, wenn man in Go statt GC so etwas wie einen Allocator pro Request verwenden könnte
    • Auch in Go sind Custom Allocators und Arenas nicht unmöglich, aber die Nutzbarkeit ist sehr schlecht und es ist schwer, sie richtig einzusetzen. Es gibt auch keine Möglichkeit, Ownership-Regeln auf Sprachebene auszudrücken oder durchzusetzen. Am Ende schreibt man also im Grunde nur C mit leicht anderer Syntax, und ohne GC ist es sogar gefährlicher als C++
  • Ich kann nachvollziehen, warum jemand sagt, „mir gefällt die Verbosity von Zig“, aber ehrlich gesagt klingt das etwas seltsam. Während C an vielen Stellen zu locker ist, verlangt Zig umgekehrt oft zu viel „Annotation Noise“ (vor allem bei expliziten Integer-Casts in Formeln). Siehe dazu diesen Beitrag. Performance-seitig ist Zig gegenüber C oft vor allem deshalb schneller, weil Zig aggressivere LLVM-Optimierungseinstellungen nutzt (-march=native, Whole-Program-Optimization usw.). Tatsächlich sind Optimierungshinweise wie unreachable auch in C über Spracherweiterungen möglich, und Clang betreibt ebenfalls sehr aggressive Constant Folding. Das heißt: Der Unterschied zwischen Zigs comptime und der Codegen-Seite von C entsteht oft durch Compiler-Optimierungseinstellungen. TL;DR: Wenn C langsam ist, sollte man zuerst die Compiler-Settings prüfen. Der Kern der Optimierung ist ohnehin LLVM
    • Beim Beispiel mit den Casts könnte man im Gegenteil einfach eine Funktion schreiben, die den Cast kapselt, und so Wiederverwendbarkeit und Absicht im Code erhöhen
      fn signExtendCast(comptime T: type, x: anytype) T {
        const ST = std.meta.Int(.signed, @bitSizeOf(T));
        const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x)));
        return @bitCast(@as(ST, @as(SX, @bitCast(x))));
      }
      export fn addi8(addr: u16, offset: u8) u16 {
        return addr +% signExtendCast(u16, offset);
      }
      
      Auch auf diese Weise erhält man exakt dasselbe Assembly, und es ist vielseitiger und klarer
    • Die Ideen von Zig sind interessant, und es legte mehr Gewicht auf comptime und Whole-Program-Compilation, als ich im ursprünglichen Artikel erwartet hatte. Dem stimme ich zu. Zur Referenz: Virgil unterstützt seit 2006 den Compile-Time-Einsatz der gesamten Sprache und Whole-Program-Compilation. Virgil zielt nicht auf LLVM, daher ist ein Geschwindigkeitsvergleich letztlich ein Vergleich der Backends. Durch diesen Ansatz kann Virgil Methodenaufrufe vorab statisch binden (devirtualisieren), ungenutzte Felder/Objekte möglichst entfernen, Konstanten sogar bis in Feld-Heap-Objekte propagieren und perfekt spezialisieren, was sehr starke Optimierungen ermöglicht
    • Mit Blick auf den künftigen AI-Einsatz glaube ich, dass immer explizitere und ausführlichere Sprachen an Bedeutung gewinnen werden. Unabhängig davon, ob man mit AI programmiert oder ob das gut ist, bevorzugen viele Entwickler die Unterstützung durch AI, und die Sprachen werden sich entsprechend verändern
    • Wenn ein neues x86-Backend eingeführt wird, könnte man künftig auch Fälle sehen, in denen der Performance-Unterschied zwischen C und Zig tatsächlich auf das Zig-Projekt selbst zurückgeht
    • Bei expliziten Integer-Casts soll bald eine Verbesserung kommen, die das Ganze sauberer macht. Siehe diese Diskussion
  • Benchmarks nach dem Muster „C ist schneller als Python“ auf Ebene der Sprache selbst sind nicht sinnvoll, aber bestimmte Sprachfeatures können einer Optimierung große Hürden in den Weg stellen. Mit der richtigen Sprache können sowohl Entwickler als auch Compiler ihre Absicht auf natürliche und schnelle Weise ausdrücken
  • Die for-Loop-Syntax von Zig wirkt auf mich viel zu unübersichtlich. Zwei Listen nebeneinanderlegen und ihre Positionen ausrichten zu müssen, tut schon beim Ansehen weh. Ich halte es für einen Fehler, dass moderne Sprachen so viele „magische“ Syntaxformen und Sonderzeichen anhäufen. Ich glaube nicht, dass ich mir das stundenlang ansehen könnte
    • Solche Muster, bei denen zwei Arrays durchlaufen werden, sind in Low-Level-Code sehr häufig, paralleles Iterieren ebenso. Daher finde ich es eher passend, dass Zig das klar und natürlich unterstützt. Ich frage mich, warum das in den Augen schmerzt
  • Optimierung ist sehr wichtig. Ihr Effekt wird mit der Zeit nur noch größer
    • Das gilt allerdings nur unter der Voraussetzung, dass die Software tatsächlich genutzt wird