Warum Rust Code mit zwei mutable Referenzen nicht optimiert: Eine Erklärung für Anfänger

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Einleitung: Das Rätsel des Rust-Compilers



Stellen Sie sich vor: Sie programmieren in Rust, Sie sind ein echter Fan von Speichersicherheit. Sie wissen, dass zwei mutable references (&mut T) auf dieselben Daten niemals gleichzeitig existieren können. Dies ist eine der Hauptregeln der Ausleihe (Borrowing Rules). Der Rust-Compiler überwacht dies streng.



Man sollte meinen, wenn der Compiler sicher ist, dass sich zwei &mut nicht überschneiden (keine Aliase sind), sollte er dieses Wissen für aggressive Optimierungen nutzen. Zum Beispiel Operationen umstellen, Werte in Registern zwischenspeichern, Speicher nicht erneut lesen. Aber hier ist das Paradoxon: Der Rust-Compiler (LLVM) tut dies oft nicht. Warum? Das erscheint doch so logisch!



Diese Frage hat tausende Aufrufe auf Stack Overflow gesammelt. Und heute, wie echte Detektive, werden wir dieses Rätsel lösen. Wir lesen nicht nur die Antwort – wir verstehen die tieferen Gründe, die mit dem C-Erbe, LLVM und der Funktionsweise der Optimierung zusammenhängen. Los geht's!



1. Die goldene Regel von Rust: Zwei &mut sind immer unterschiedliche Daten



Erinnern wir uns an die Grundlagen. In Rust gibt es zwei Arten von Referenzen:

  • Unveränderlich (&T): Es kann beliebig viele geben, aber sie können nicht geändert werden.
  • Veränderlich (&mut T): Es kann nur eine geben, aber sie kann geändert werden.


Diese Regel ist noalias für &mut. Der Compiler könnte theoretisch annehmen: "Wenn ich zwei &mut i32 sehe, zeigen sie definitiv auf unterschiedliche Speicherstellen."



Aber schauen wir uns ein einfaches Beispiel an, das das Problem zeigt:



fn example(x: &mut i32, y: &mut i32) -> i32 {    *x = 42;    *y = 10;    *x // Erwarten 42, aber...}


Hier ist alles klar: x und y sind unterschiedliche Variablen. Rust garantiert, dass sie sich nicht überschneiden. Der Compiler könnte einfach 42 zurückgeben, ohne die Zuweisungen auszuführen. Aber ist das wirklich so?



2. Das Problem: LLVM und seine "Blindheit" gegenüber Rusts Garantien



Der Hauptakteur unserer Geschichte ist LLVM (Low Level Virtual Machine). Rust verwendet LLVM als Backend zur Generierung von Maschinencode. LLVM ist ein leistungsstarker Optimierer, aber er kennt die Regeln von Rust nicht. Er kennt nur die Regeln von C/C++.



In C/C++ gibt es das Schlüsselwort restrict (in C99), das dem Compiler sagt: "Diese Referenz ist eindeutig, niemand sonst zeigt auf diese Daten." Aber in C/C++ ist restrict ein Versprechen des Programmierers, keine Garantie des Compilers. Wenn der Programmierer das Versprechen bricht, ist das undefiniertes Verhalten.



Rust hingegen gibt eine stahlharte Garantie. Aber um diese Garantie an LLVM weiterzugeben, muss das Rust-Frontend &mut explizit mit dem Attribut noalias im LLVM IR (Zwischendarstellung) markieren.



Und hier wird es interessant. Rust markiert &mut nur in bestimmten Fällen als noalias. In manchen Fällen jedoch nicht. Warum?



2.1. Historisches Problem: UnsafeCell und Unsafe-Code



Innerhalb von Rust gibt es den Typ UnsafeCell<T>. Dies ist der Baustein für Cell, RefCell, Mutex und andere Typen, die innere Veränderbarkeit (Interior Mutability) ermöglichen.



Wenn Sie ein &mut UnsafeCell<i32> haben, können Sie darüber einen rohen Zeiger erhalten und die Daten sogar über eine unveränderliche Referenz ändern. Das bricht die noalias-Garantie!



Um undefiniertes Verhalten zu vermeiden, setzt der Rust-Compiler noalias nicht auf &mut, wenn dahinter ein UnsafeCell stecken könnte. Aber das Problem ist tiefer.



3. Praxisbeispiel: Warum die Optimierung nicht greift



Kehren wir zur Funktion example zurück.

Blogs

Buchempfehlungen