Einleitung: Warum Rust-Performance keine Magie, sondern Disziplin ist
Rust gilt zu Recht als eine der schnellsten Systemprogrammiersprachen. Allerdings arbeitet „roher" Code, wie in jeder anderen Sprache auch, nicht immer an der Leistungsgrenze. Viele Einsteiger, die von Python oder JavaScript zu Rust wechseln, erwarten einen sofortigen Geschwindigkeitsschub, vergessen aber, dass Optimierung ein Prozess und kein Geschenk des Compilers ist.
Dieser Artikel richtet sich an diejenigen, die bereits ihre ersten Programme in Rust geschrieben haben und verstehen möchten, wie sie das Maximum daraus herausholen. Wir werden drei Schlüsselbereiche behandeln: Profiling (um zu wissen, was behoben werden muss), Zero-Cost-Abstraktionen (um nicht für das zu bezahlen, was man nicht nutzt) und Speicherverwaltung (um Engpässe zu vermeiden). Alle Beispiele werden von Code begleitet, der sofort in Ihrer Umgebung ausgeführt werden kann.
1. Profiling: Bevor du optimierst, messe
Die Hauptregel der Optimierung: Optimiere nicht blind. Ohne Profiling riskierst du, Stunden mit der „Beschleunigung" von Code zu verbringen, der ohnehin schnell läuft, während der eigentliche Engpass unbemerkt bleibt.
Werkzeuge für das Profiling
Im Rust-Ökosystem gibt es mehrere hervorragende Werkzeuge. Für den Anfang empfehle ich cargo-flamegraph – es visualisiert, wo das Programm die meiste Zeit verbringt. Installation:
cargo install flamegraphStarten des Profilings für Ihre Binärdatei:
cargo flamegraph --bin my_appNach der Ausführung erhalten Sie eine SVG-Datei mit einem „Flame-Graphen". Breite „Balken" zeigen die Funktionen an, die die meiste CPU-Leistung verbrauchen.
Beispiel: Einen Engpass finden
Angenommen, wir haben eine Funktion, die die Quadrate von Zahlen summiert:
fn sum_of_squares(n: u64) -> u64 { let mut sum = 0; for i in 1..=n { sum += i * i; } sum}Wenn der Profiler anzeigt, dass 90 % der Zeit für diese Funktion aufgewendet wird, wissen wir, wo wir hinschauen müssen. Wenn der Engpass hingegen bei der Ein-/Ausgabe liegt (z. B. beim Lesen einer Datei), bringt die Optimierung der Berechnungen kein Ergebnis.
2. Zero-Cost-Abstraktionen: Wie der Compiler hilft (und hindert)
Eines der Hauptmerkmale von Rust ist das Konzept der Zero-Cost-Abstraktionen. Das bedeutet, dass hochrangige Konstrukte (Iteratoren, Closures, Traits) im Vergleich zum manuellen Schreiben von Schleifen keinen zusätzlichen Overhead verursachen sollten. Dies funktioniert jedoch nur bei korrekter Verwendung.
Iteratoren vs. Schleifen
Viele Anfänger schreiben so:
let v: Vec<i32> = (0..1000).collect();let mut result = Vec::new();for i in &v { result.push(i * 2);}Dieser Code kann mit Iteratoren umgeschrieben werden, und der Rust-Compiler optimiert ihn auf denselben Maschinencode:
let v: Vec<i32> = (0..1000).collect();let result: Vec<i32> = v.iter().map(|x| x * 2).collect();Wenn Sie außerdem filter, map und fold nacheinander verwenden, kann der Compiler sie zu einem einzigen Durchlauf „verschmelzen" – dies wird als Loop Fusion bezeichnet. Manuelle Schleifen bieten eine solche Optimierung nicht.
Box<dyn Trait> vs. Generics
Ein weiterer häufiger Fehler ist der übermäßige Gebrauch von Trait-Objekten (Box<dyn SomeTrait>). Sie verursachen eine dynamische Dispatch, was zusätzlichen Overhead bedeutet. Wenn die Anzahl der Varianten zum Zeitpunkt der Kompilierung bekannt ist, verwenden Sie Generics (statischer Dispatch):
// Schlecht: dynamischer Dispatchfn process(items: Vec<Box<dyn Processor>>) { for item in items { item.run(); }}
// Gut: statischer Dispatchfn process<T: Processor>(items: Vec<T>) { for item in items { item.run()