Ich mag Rust schon seit langer Zeit; seine Vorteile sind offensichtlich, und seine Nachteile sind ebenso klar. Es bietet Speicher‑Safety ohne GC, 零成本抽象, 并发安全, vermutlich das beste Werkzeug‑Chain und 配套外围建设, eine enorme Abdeckung (von Web 云服务器 bis zu 单片机). Außerdem ist das Fehlerbehandlungskonzept hervorragend. Die Nachteile: eine steile Lernkurve? Schwierigkeit, Ownership, Borrow und ähnliche Modellbeschränkungen zu verstehen; und das Schreiben von Code ist langsam?
Seit langem hielten wir dies für einen Nachteil von Rust, doch tatsächlich lassen sich die ersten beiden Probleme durch das Trainieren von Denken und Erkenntnis lösen. Die langsame Kompiliergeschwindigkeit hingegen bremst meinen Arbeitsablauf kontinuierlich aus. Vor einigen Jahren schien das noch in Ordnung zu sein; bevor KI die Szene eroberte, hatte ich Geduld und schrieb oft Algorithmen oder Features bis in die frühen Morgenstunden. Aber jetzt? Ich kann kaum behaupten, dass ich unverändert geblieben bin. Ich bin völlig ungeduldig geworden, habe keine Lust mehr, darüber nachzudenken, wie man Rust‑Code schreibt, und kann die lange Fortschrittsanzeige von Cargo nicht abwarten, besonders wenn der ld‑Teil keinerlei Fortschritt zeigt und einfach hängen bleibt – das ist wirklich unangenehm.
Gibt es für all das denn wirklich keine Lösung? Tatsächlich gibt es eine. Vor etwa einem Monat habe ich einige Methoden ausprobiert, um meinen Rust-Entwicklungsprozess zu beschleunigen, darunter Workflow-Optimierungen und dergleichen, aber letztendlich hing die Wartezeit jedes Mal an Cargo. Wenn man dieses Problem nicht löst, geht man nicht an die Ursache heran. Da trifft es sich gut, diese Gelegenheit zu nutzen, um ein wenig über die Optimierung der Cargo.toml-Konfiguration bei der Rust-Entwicklung und die dazugehörigen peripheren Konfigurationen zu sprechen.
Backend-Theorie
In Rusts Design ist LLVM zwar etwas nachlässig, aber die Wahl ist gut; im Gegensatz zu Go, das den Prozess der Code‑Generierung selbst verwaltet, ist es sehr vorteilhaft, die ausgereiften Optimierungen des großen LLVM‑Ökosystems zu nutzen. Der gesamte Rust‑Kompilierungsablauf lässt sich grob folgendermaßen beschreiben: Der Quellcode wird zunächst lexikalisch analysiert und syntaktisch geparst, wodurch ein AST entsteht. Anschließend erfolgt die Makroexpansion und das Herabstufen des HIR, wodurch das Ganze abgeflacht wird. Dann folgen die regulären Typprüfungen und ein wenig Borrow‑Checking, schließlich wird MIR erzeugt – bis zu diesem Punkt arbeitet das Frontend des Rust‑Compilers. Die Aufgaben nach MIR werden an LLVM übergeben, zum Beispiel die Übersetzung von MIR in LLVM‑IR, das Durchlaufen der LLVM‑Optimierungspässe (von O0 bis O3) und schließlich die Erzeugung des Ziel‑Maschinencodes, der vom Linker zu einer ausführbaren BIN‑Datei zusammengefügt wird.
Wo liegt also eigentlich das Problem? Die Antwort ist im Grunde sehr einfach: LLVM ist eine sehr schwere Infrastruktur – in Produktionsumgebungen ist dieses Gewicht richtig, denn Kompilierungszeit gegen Ausführungszeit einzutauschen ist eine hervorragende Philosophie –, und sie schlägt die Brücke für das darüberliegende Rust sowie eine ganze Reihe anderer Sprachen wie C, C++ und Swift. Das bedeutet, dass ihre Optimierungspipeline darauf ausgelegt ist, „optimalen Maschinencode zu generieren“, und nicht darauf, „die Kompilierung schnell einzuschließen“. Diese dutzenden Optimierungs-Passes im release-Modus einmal durchzulaufen, ist extrem zeitaufwendig; selbst im debug-Modus unter O0 muss LLVM dennoch den gesamten Prozess der IR-Generierung und Maschinencodegenerierung durchlaufen, und dieser Prozess selbst ist keineswegs leichtgewichtig. Hinzukommt, dass Rusts Monomorphisierung von Generics zur Kompilierzeit riesige Mengen an Code auflöst, sodass das an LLVM übergebene IR-Volumen weit größer ist, als es der Quellcode vermuten lässt, wodurch die von LLVM zu verarbeitende Arbeitsmenge natürlicherweise drastisch anschwillt.
Cranelift
Im Kern liegt also ein großer Teil der langen Kompilierzeit nicht am Rust‑Frontend, sondern am LLVM‑„schweren Backend“. Das wirft die Frage auf: Kann man es entfernen? Die Antwort lautet ja, miau. Es gibt ein Backend namens Cranelift, das genau für diesen Bedarf entwickelt wurde. Es begann als Cretonne, 2016 ins Leben gerufen, entwickelt von der Bytecode Alliance. Ursprünglich wurde es als Code‑Generierungs‑Backend für das Wasmtime‑Paket konzipiert; später hat das Rust‑Team es als optionales Codegen‑Backend übernommen.

Wo also sollte Cranelift eigentlich schnell sein? Die beste Phase ist die Entwicklungszeit. Zu diesem Zeitpunkt braucht man vielleicht gar keinen nahezu perfekten, auf maximale Ausführungseffizienz optimierten Maschinencode; vielleicht brauchen wir einfach nur schnelles Feedback, solange dieser nicht ganz so tolle Maschinencode semantisch äquivalent zu dem sauber von LLVM generierten Maschinencode ist. In der Anfangszeit konnte Cranelift das eigentlich nicht leisten, weil es viele Edge Cases gab. Obwohl es sie immer noch gibt, hat es sich mittlerweile stark verbessert. Mittlerweile ist die Wahrscheinlichkeit, dass Cranelift läuft, aber LLVM fehlschlägt, ungefähr so groß wie das Auslösen eines rustc ICE beim Schreiben von Rust. Um extrem optimierten Maschinencode zu generieren, führt LLVM Dutzende von Optimierungs-Passes aus, wie z. B. Schleifenentfaltung, Vektorisierung, Konstantenpropagation, Eliminierung von totem Code und so weiter ... sehr, sehr viele, Schicht für Schicht geschliffen. Die Designphilosophie von Cranelift ist dagegen völlig anders: Es reduziert diese Optimierungsschritte drastisch, führt nur die grundlegendste Registerbelegung und Befehlsauswahl durch und schließt die Codegenerierung in einem einzigen linearen Scan ohne wiederholte Iterationen ab. Gleichzeitig ist das IR-Design schlanker und speziell für die schnelle Übersetzung von höherer IR in Maschinencode konzipiert, anstatt wie LLVM IR die Altlasten jahrzehntelanger Generalisierung mit sich herumzuschleppen.
Nachdem wir nun über die Vorteile gesprochen haben, was ist der Preis dafür? Erstens ist offensichtlich, dass der von Cranelift generierte Code bei der Laufzeit-Performance schlechter abschneidet als LLVM und je nach Szenario etwa 10 % bis 30 % langsamer ist. Aber wenn man mal ehrlich ist, erkennt man schnell, dass das in der Entwicklungsphase überhaupt keine Rolle spielt. Wozu brauche ich denn so viel Speed? Machen Sie Benchmarks oder einen CI release? Ich will doch nur verdammt nochmal, dass cargo build schneller läuft, um zu sehen, ob die Logik funktioniert. Außerdem wette ich, dass die meisten Programme, die Sie schreiben, kaum mehr als 80 % der Zeit die Rechenressourcen voll auslasten. Es kommt ja immer Geschäftslogik rein, aber wie groß kann diese Last in der Entwicklung schon sein? Die CPU ist doch wahrscheinlich die ganze Zeit nur am Däumchen drehen! Ich will stattdessen Ergebnisse sehen und die Logik überprüfen. Sollte man diese Rechnung – Kompilierzeit gegen Laufzeiteffizienz – im Entwicklungszyklus also nicht genau andersherum aufstellen?
Codegen-Einheiten
Betrachten wir außerdem codegen-units. Dabei handelt es sich um einen Parameter, der steuert, in wie viele kleinste Einheiten der Compiler ein Crate für die parallele Verarbeitung im Backend aufteilt. Standardmäßig sind das im Debug‑Modus 256 Einheiten und im Release‑Modus 16 Einheiten. Je größer die Zahl, desto höher die Parallelität und desto schneller die Kompilierung – weil mehrere CPU‑Kerne gleichzeitig für die Code‑Generierung im Backend genutzt werden können. Der Nachteil ist jedoch eine schlechtere Optimierung, da jede Einheit unabhängig optimiert wird; LLVM (oder Cranelift) hat dadurch einen kleineren Kontext, und Möglichkeiten für Inline‑ und Optimierungen über Einheiten hinweg sinken.
Aber bei einem echten Release sollte man im Allgemeinen codegen-units = 1 verwenden, eine extreme Einstellung; nur so lässt sich das Ergebnis maximal optimieren. Schließlich nutzt man Rust – es ist doch selbstverständlich, dass man die Kompilierzeit gegen die Laufzeitleistung eintauscht.
Optimierungsstufe
Eine weitere Konfiguration, die oft übersehen wird, aber angepasst werden kann, ist opt-level. Standardmäßig sollte sie "3" sein, aber für Release-Builds ist es üblicher, sie auf "z" zu stellen. Das ist eine kostenlose Optimierung der Artefaktgröße, ohne den Code ändern zu müssen – warum sollte man sie also nicht aktivieren? Im Entwicklungsmodus sollte hier jedoch "0" konfiguriert werden, da nur ganz ohne Optimierung die schnellste Geschwindigkeit erreicht wird.
Zusätzlich gibt es einen kleinen Trick: Cargo.toml kann tatsächlich unterschiedliche Optimierungsstufen für Abhängigkeiten und den eigenen Code festlegen.
[profile.dev.package."*"]
opt-level = 3Auf diese Weise können Sie Drittanbieter-Abhängigkeiten mit O3 kompilieren. Dies erhöht lediglich die Zeit für die erste Kalt-Kompilierung, da spätere Erstellungen inkrementell erfolgen. Da externe Abhängigkeiten in der Regel nicht häufig aktualisiert werden, bietet die Verwendung von O3 für Abhängigkeiten und O0 für Ihren eigenen Geschäftscode unter normalen Umständen ein gutes dev-Modus-Erlebnis, das Ausführungsgeschwindigkeit und Dateigröße gut gegeneinander abwägt. Im Release-Modus empfehle ich jedoch weiterhin ohne Umschweife, O3 oder Z voll auszureizen.
Linkzeit-Optimierung
LTO(Link‑Time Optimization)ist ein weiteres Feature, das sehr viel Speicher und Kompilier‑Performance beansprucht, und die meisten Projekte empfehlen, es für Release‑Builds zu aktivieren. Bei einer normalen Kompilierung wird jedes Crate unabhängig optimiert, und das Backend kann die Aufrufbeziehungen zwischen den Crates nicht sehen, sodass bestimmte Cross‑Crate‑Inlinings und Dead‑Code‑Eliminierungen nicht möglich sind – ein sehr häufiges Szenario. LTO bricht diese Grenze, indem der Optimierer beim Linken den IR aller Crates erhält und eine globale Optimierung durchführt. Aber setzen Sie jedoch niemals gedankenlos lto = true im Profil, denn das lässt Ihre Rust‑Kompilierung so langsam werden, dass Sie das Leben hinterfragen; fügen Sie es einfach nur hinzu, wenn Sie das Release‑Profil verwenden.
Debug‑Symbole entfernen
Wie bei allen anderen Dingen auch befinden sich die debuginfo und symbols von Rust genau hier. Das Aktivieren von strip im release-Modus kann die Größe erheblich reduzieren – ein gewaltiger Unterschied wie (normalerweise 50MB vs 5MB). Das Entfernen von symbols ist außerdem sicherer; schließlich wird Frontend-source code meistens dadurch geleakt, dass maps versehentlich auf npm gepusht werden :(
Also gibt es im dev‑Profil nicht viel zu sagen; man kann debuginfo nicht einfach strippen, sonst kann man gdb / lldb nicht normal debuggen und im Backtrace erscheinen keine sinnvollen Funktionsnamen. Beachte jedoch, dass beim Packen unter ArchLinux standardmäßig strip ausgeführt wird, was mit uns kollidiert, weil wir bereits vorher strippen und später Fehler auftreten können. Wenn du ein AUR‑Paket schreibst, kannst du diesen Schritt explizit überspringen.
Verzicht auf panic!
In normaler Logik sollte panic eigentlich nicht existieren. Wenn normaler Code zur runtime auf einen Fehler stößt, sollte jeder für Fehlertoleranz entworfene Err sicher zurückgegeben werden, anstatt gewaltsam zu panicken. Genau wie bei einer React ErrorBoundary sollte er ganz normal als Fehler behandelt werden. Ein panic sollte nur ausgelöst werden, wenn der Code felsenfest davon überzeugt ist, einen unmöglichen und nicht mehr zu rettenden Zustand erreicht zu haben. Meine persönliche Philosophie ist es, auf panic zu verzichten. Warum? Weil man Code in der Praxis in 3 Testebenen unterteilen kann: Die erste Ebene ist der happy path, die zweite Ebene sind Fehlerpfade (eine von unendlich vielen Möglichkeiten) und erst die dritte Ebene sind Edge Cases, die durch fuzzing oder Kollisionstests erschöpfend ermittelt werden. Aus technischer Sicht lässt sich der fehlerfreie Pfad zu 100 % testen, und bei Fehlerpfaden reicht 1 von n Varianten einer Kategorie. Kollisionstests sind reine Zeitsache und meistens den Aufwand nicht wert. Wenn die Zahl Ihrer Nutzer wirklich groß genug ist, werden Sie selbst wissen, wann es Zeit dafür ist; ich persönlich verzichte in der Regel komplett darauf.
Für guten Code sollte man Tests für einen korrekten Pfad und mindestens einen fehlerhaften Pfad abdecken. Sobald ein fehlerhafter Pfad abgedeckt ist, kann man den Fall testen, dass ein Err zurückgegeben wird, ein Fallback‑Logik schreiben und dadurch natürlich thiserror auflisten oder anyhow abfangen und die Log‑Einträge glätten. Kurz gesagt zwingt dein Code dich, Mechanismen für diese Situationen zu implementieren; wenn du diese Ebene erreicht hast, ist panic für dich überflüssig – man kann es als „theoretisch unmöglich“ interpretieren, aber das bleibt ebenfalls nur theoretisch. In der realen Welt gibt es OS‑Fehler, Speicherfehler, durch kosmische Strahlung verursachte Bit‑Flip‑Einzelereignisse, seltsame Überläufe … Man kann diese Fälle niemals vollständig abdecken. Warum also auf panic verzichten? Weil diese Situationen zu 90 % nicht durch deinen Code verursacht werden. Der Kern von panic ist, dass Rust beim Auftreten den Aufruf‑Stack zurückverfolgt, frameweise die Destruktoren (drop) aufruft und Ressourcen freigibt. Dafür muss der Compiler zusätzliche unwind‑Tabellen erzeugen, die dir helfen, logische Fehler in deiner Geschäftslogik zu finden. Wenn die Fehler größtenteils nicht deine Schuld sind, hat diese Information kaum Wert, sie vergrößert zudem das Artefakt und belastet den Linker – das Abschalten ist die richtige Lösung. Sobald dein Code ausreichend getestet und du zuversichtlich bist, kannst du panic = abort konfigurieren.
Tatsächlicher Benchmark
All das Gerede ist sinnlos; schauen wir uns tatsächlich die Verbesserungen durch diese Kombinationen an. Hier nehme ich ein kleines Spielzeugprojekt, das ich vor ein paar Monaten gemacht habe, als Beispiel.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Language Files Lines Code Comments Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CSS 5 152 113 16 23
Go 77 14556 12409 584 1563
JavaScript 23 2506 1999 232 275
JSON 93 1780 1780 0 0
Just 1 408 275 71 62
Makefile 1 15 8 3 4
Shell 16 1440 1075 166 199
SVG 1 9 9 0 0
TOML 20 502 419 3 80
TSX 75 3257 2718 132 407
TypeScript 390 36643 30350 1627 4666
─────────────────────────────────────────────────────────────────────────────────
HTML 2 41 32 7 2
|- CSS 1 37 37 0 0
(Total) 78 69 7 2
─────────────────────────────────────────────────────────────────────────────────
Markdown 95 5344 0 3578 1766
|- BASH 3 8 8 0 0
|- Go 2 21 21 0 0
|- HTML 1 25 15 10 0
|- JSON 6 317 317 0 0
|- Rust 1 10 9 0 1
|- TSX 1 11 9 0 2
(Total) 5736 379 3588 1769
─────────────────────────────────────────────────────────────────────────────────
Rust 181 31654 26951 1032 3671
|- Markdown 86 581 0 564 17
(Total) 32235 26951 1596 3688
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total 980 99317 78554 8025 12738
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━In diesem Projekt beträgt der Rust‑Teil etwa 32 K SCoL Rust, ohne die Größe der Abhängigkeiten.
Dieses Repository ist tatsächlich ein Monorepo mit vielen Unterprojekten, doch darin lassen sich zwei typische Beispiele herausfiltern. Zunächst ein Blick auf das Skeleton‑Paket: Seine Merkmale sind offensichtlich – ein großer Codeumfang bei fast keinen Abhängigkeiten, wodurch es den höchsten Anteil an Code‑Generierung im gesamten Projekt ausmacht und sich hervorragend für Cranelift eignet. Darüber hinaus ist das Paket sehr „clean“, wobei „clean“ hier bedeutet, dass es rein aus sicherem Rust besteht.
Im Gegensatz dazu ist das unten stehende CLI-Paket nicht besonders geeignet. Viele Abhängigkeiten sind zwar kein Problem – theoretisch spiegelt dies eine höhere Effizienz wider –, aber hier gibt es eine klassische Entweder-Oder-Entscheidung. Man kann oben links sehen, dass die Abhängigkeit ring als optional markiert ist; das liegt daran, dass das Projekt ursprünglich aws-lc-rs verwendet hat. Beide dieser Dinge sind Verschlüsselungsalgorithmus-Backends in Rust. Warum sollte es jedoch optional sein? Das liegt daran, dass ring in reinem Rust geschrieben ist, während das andere über Asm-Assembler-FFI eingebunden ist und speziell für die Assembler-Beschleunigung auf gängigen cpu-Architekturen wie x86 und arm64 entwickelt wurde. Doch genau hier liegt der Haken: Die Magie von Cranelift beschränkt sich rein auf reines Rust. Sobald man Unsafe Code oder FFI C, ASM und Ähnliches einführt, wird die Kompatibilität von Cranelift in der Praxis extrem schlecht.
Das ist jedoch nicht unlösbar; man kann tatsächlich mit cfg das Backend auswählen und im Cranelift‑Modus ring an den Compiler übergeben. Nachdem man die dev‑ und release‑Profile wie oben beschrieben konfiguriert hat, kann man einfach einen CLI‑Kompilierungsvergleich durchführen. In diesem Fall ist die Entwicklung mindestens dreimal so schnell wie das Release, selbst bei Kaltkompilierung ohne inkrementelle Builds; und bei Verwendung von O0 und deaktiviertem LTO sollte das dev‑Profil bei nachfolgenden inkrementellen Updates um ein Vielfaches schneller sein als das Release. Da LTO‑artige Magie tatsächlich durch das Flachlegen der Grenzen einzelner Crates entsteht, wird beim Zusammenführen alles zu einer Einheit, und eine Codeänderung zwingt die komplette Rekompilierung, wodurch korrekte inkrementelle Updates verhindert werden.
![Dies ist ein Screenshot eines Terminalfensters mit dem Titel „~/C/P/seam“, das das Ende eines `cargo build --release`-Durchlaufs für das Projekt „seam“ zeigt. Die Bildlaufleiste listet Dutzende von Abhängigkeits‑Crates auf, die nacheinander kompiliert werden – walkdir, rand, tungstenite, sha2, toml, notify, rquickjs, clap und weitere – gefolgt von den eigenen Arbeitsbereichs‑Mitgliedern des Projekts wie seam-codegen, seam-skeleton, seam-injector-wasm, seam-engine-wasm, seam-server, seam-server-axum, seam-cli und mehreren Beispiel‑Backends (demo-server-rust, github-dashboard-axum, markdown-demo-rust, i18n-demo-axum). Die letzten Zeilen bestätigen den Erfolg: „Finished `release` profile [optimized] target(s) in 1m 05s“, gefolgt von einer Leerlauf‑Shell‑Eingabeaufforderung `canmi@xyy ~/C/P/seam (main)>`. Es ist ein Beweis für einen sauberen, vollständigen Release‑Build eines mehr‑Crate‑Rust‑Monorepos mit WASM‑ und Axum‑Server‑Komponenten.](https://cdn.ffoni.com/image/e8b530aa153a78f1cdea3d2980f80ecd.jpeg)
![Ein Terminal‑Screenshot zeigt das Ende eines Cargo‑Build‑Logs für einen Rust‑Workspace namens „seam“, der mit `~/C/P/seam` bezeichnet ist und in dem die Shell unter dem Benutzer `canmi` auf dem Branch `main` läuft. Dutzende von Abhängigkeits‑Crates werden nacheinander kompiliert (console, reqwest, clap, tokio‑tungstenite, notify, indicatif und weitere), gefolgt von den eigenen Crates des Projekts — seam-codegen, seam-server-axum, seam-skeleton, seam-injector-wasm, seam-engine-wasm, seam-cli, alle in Version 0.5.38 — sowie mehreren Beispiel‑Backends (github-dashboard-axum, i18n-demo-axum, markdown-demo-rust, demo-server-rust). Das Log endet mit `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 19.23s` und weist damit auf einen erfolgreichen Debug‑Build des gesamten Workspaces einschließlich seiner Beispielanwendungen hin.](https://cdn.ffoni.com/image/71aa2631cd4c616d2ecca6e510cf06f4.jpeg)
Betrachtet man die Gesamtergebnisse, unterscheiden sich die beiden Dinge eigentlich kaum. Es ist zwar kein Quantensprung, aber definitiv deutlich spürbar. Der Hauptgrund dafür ist, dass Apple Silicon einfach extrem stark ist – die Single-Core-Leistung und die Speicherbandbreite der M-Chips sind unfassbar hoch, wovon rechen- und I/O-intensive Aufgaben wie das Kompilieren genau profitieren. Wenn man das Ganze auf Linux laufen lässt, wäre der Unterschied normalerweise noch größer; wenn man jedoch Asahi Linux oder Ähnliches auf einem MacBook installiert, gewinnt definitiv Linux.
Veralteter Linker
Unter Linux nutzt Rust standardmäßig GNU ld (bfd) – ja, genau den ältesten und langsamsten. Da es single-threaded ist und Symbolauflösung sowie Relokation seriell scannend verarbeitet, bremst es bei größeren Projekten spürbar aus. Unter Linux können Sie das von @Rui Ueyama geschriebene mold ausprobieren; die Konfiguration ist sehr einfach und erfordert nur das Hinzufügen einer Zeile in .cargo/config.toml.
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
Die Verbesserung ist immer noch sehr deutlich, aber leider gibt es auf macOS nur sold als kommerzielles Projekt. Die gute Nachricht ist jedoch, dass lld auf macOS oder Apples mitgeliefertes ld64 selbst nicht mehr langsam ist – besonders der neue Linker seit Xcode 15 bringt eine extrem deutliche Geschwindigkeitssteigerung. Das einzige Manko an diesem Ding ist jedoch, dass man bei unbeaufsichtigter macOS-CI und nach Updates jedes Mal per sudo Accept einer Vereinbarung zustimmen muss, was dazu geführt hat, dass meine geplanten CI-Tasks etliche Male fehlgeschlagen sind.
MUSL
Ich persönlich bin ein riesiger Fan von MUSL und hasse GNU glibc wirklich, aber für die Entwicklung empfehle ich dir trotzdem, x86_64-unknown-linux-gnu zu verwenden. Warum? Weil es schnell ist. Der größte Pluspunkt von MUSL ist, dass es vollständig statisch verlinkte Binärdateien erzeugen kann – ohne von irgendwelchen dynamischen Bibliotheken auf dem System abzuhängen. Das Kompilierte lässt sich auf jede beliebige Linux-Maschine kopieren und ausführen, was es besonders für Container und Embedded-Systeme geeignet macht. Wo es jedoch langsam ist, liegt es ebenfalls am statischen Verlinken: Da der Linker alles zusammenpacken muss, ist der Arbeitsaufwand in der Link-Phase deutlich größer als beim dynamischen Verlinken.
Die gute Nachricht ist jedoch: Wenn Sie macOS verwenden, müssen Sie sich überhaupt keine Sorgen machen, da aarch64-apple-darwin derzeit die einzige Wahl ist. macOS unterstützt von seinem Design her keine vollständig statische Verlinkung, was aus Sicht der Kompilierungsgeschwindigkeit sogar eine gute Sache ist. Daher nutzt das dev profile unter Linux gnulibc und unter macOS libSystem, während für das release profile sowohl unter macOS als auch unter Linux musl verwendet werden kann. Aufgrund von Problemen mit der Standardauswahl von linker und ar unter macOS ist das Einzige, worauf man achten sollte, dass eventuell .cargo/config.toml konfiguriert werden muss.
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
ar = "x86_64-linux-musl-ar"Übrigens, bitte verwenden Sie zig überhaupt nicht; das zbuild von nightly Rust hat derzeit erhebliche Probleme. Wenn Sie Kreuzkompilierung benötigen und der Host Linux x86 ist, empfehlen wir cross. Das Bauen in Docker funktioniert hervorragend und liefert eine vollständige Umgebung. Wenn Sie glibc bevorzugen, ist das ebenfalls gut, weil glibc nur rückwärtskompatibel ist. Üblicherweise kompiliert man gegen die glibc‑Version einer großen Debian‑Veröffentlichung (z. B. Debian‑2). Andernfalls führt eine zu neue glibc dazu, dass viele Maschinen nicht laufen; das liegt nicht an einem neuen Feature, sondern einfach daran, dass man anhand des Versionsstrings prüft und Sie damit ärgert. Ist der Host mac, nutzen Sie bitte das native rustup target + cargo build --release .
UPX ist ein schlechtes Ding
Ein weiterer Punkt, der eine Diskussion wert ist, ist die Verwendung von UPX; ich weiß, dass es eigentlich ziemlich widersprüchlich klingt und keinen Sinn zu ergeben scheint, wenn ich sage, dass ich musl mag, aber gleichzeitig eine geringe Größe anstrebe...
ooooo ooo ooooooooo. ooooooo ooooo
`888' `8' `888 `Y88. `8888 d8'
888 8 888 .d88' Y888..8P
888 8 888ooo88P' `8888'
888 8 888 .8PY888.
`88. .8' 888 d8' `888b
`YbodP' o888o o888o o88888o
The Ultimate Packer for eXecutables
Copyright (c) 1996-2026 Markus Oberhumer, Laszlo Molnar & John Reiser
https://upx.github.ioIn der Praxis löst das Vorhandensein von UPX dieses Problem jedoch. Das Prinzip von UPX besteht darin, Ihre BIN-Datei zu komprimieren und mit einem Entpackungs-Stub zu versehen. Zur Laufzeit wird sie zuerst im Arbeitsspeicher entpackt und dann ausgeführt. Dadurch lässt sich die Binärgröße auf etwa 30 % bis 50 % des Originals reduzieren, was einen sehr deutlichen Größenvorteil bietet. Doch genau aus diesem Grund führt UPX + dynamische Verlinkung mit GNU glibc zu großen Problemen, da dynamisch verlinkte glibc-Binärdateien spezielle Sections und dynamische Lademechanismen enthalten, die durch die UPX-Komprimierung beschädigt werden können, sodass dynamische Bibliotheken zur Laufzeit nicht gefunden werden segment fault; musl hingegen eignet sich hervorragend für UPX. Es ist von Haus aus vollständig statisch und intern stabil, sodass Entpacken und Komprimieren nach UPX sehr sauber ablaufen. Daher wäre ein zusätzlicher UPX-Schritt in der Release-CI für musl tatsächlich ein großes Plus.
Außerdem sollte man UPX nicht übermäßig einsetzen. UPX eignet sich nur für langlaufende Aufgaben oder selten ausgeführte Aufgaben. Bei langlaufenden Aufgaben liegt das daran, dass UPX beim Start einmal im Arbeitsspeicher entpackt werden muss; dieser Prozess verläuft zwar vollautomatisch, erfordert jedoch eine kurze Kaltstartzeit. Das fällt kaum ins Gewicht, ist aber auf hochfrequenten Pfaden fatal. Daher eignet sich UPX sehr gut für langlaufende Aufgaben, die einmal gestartet und im Hintergrund betrieben werden. Ich rate jedoch davon ab, Docker-Images für solche dauerhaften Dienste gedankenlos mit UPX vollzupacken, da man hierbei im Grunde Arbeitsspeicher gegen Speicherplatz eintauscht – ein extrem schlechter Deal. Zudem erreicht die Komprimierung von Container-Layern beim Verteilen genau denselben Zweck wie UPX, um die Download-Größe zu reduzieren. Empfohlen wird UPX stattdessen für bestimmte CLI-Anwendungsfälle: Es spart enorm viel Speicherplatz, und die Entpackzeit beim Start ist kaum spürbar. Außerdem eignet sich musl von Natur aus hervorragend für CLI-Tools – andernfalls müsste ich namentlich AUR-Helper kritisieren: Das in Go geschriebene Ding war nicht statisch gelinkt, was dazu führte, dass bei mir einmal yay auf ArchLinux kaputtging, weil dynamische Bibliotheken nicht gefunden wurden. Da es selbst ein Paketmanager ist, gab es keine Rettung, außer über eine LiveISO zu booten und am Ende per chroot das System zu reparieren.
Zusammenfassung
Obwohl diese nützlichen Konfigurationen dir dabei helfen, Dev‑ und Release‑Profile schnell zu unterscheiden, werden ihre Vorteile mit wachsenden Projektdateien immer deutlicher. Trotzdem empfehle ich, eine CI‑Pipeline einzurichten, die den Rust Stable LLVM‑Build ausführt, am besten zusammen mit allen deinen Tests. So musst du lokal nicht mehr zu einer Nightly‑Rust-Version wechseln, Edge Cases können dich nicht mehr überraschen, und beim Pushen werden mögliche Probleme von der CI abgefangen. Das ist momentan das angenehmste DX‑Setup; das war's für jetzt, beim nächsten Mal, wenn ich Zeit habe, schaue ich wieder drüber.