Das Geheimnis von i128 auf 64-Bit-Systemen: Wie Rust mit riesigen Zahlen arbeitet

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: Große Zahlen in einer kleinen Welt



Stellen Sie sich vor: Sie schreiben einen kryptografischen Algorithmus, eine Simulation physikalischer Prozesse oder berechnen einfach die Entfernung zur nächsten Galaxie in Nanometern. Und plötzlich stellen Sie fest – der Standard-i64 (von -9 Trillionen bis 9 Trillionen) reicht Ihnen katastrophal nicht aus. In Rust gibt es einen Rettungsring – den Typ i128. Aber wie funktioniert er überhaupt auf einem Prozessor, der physikalisch nicht mit 128-Bit-Zahlen arbeiten kann? Ist das Magie?



Spoiler: Es gibt keine Magie, es gibt geniale Ingenieurskunst. In diesem Artikel werden wir analysieren, wie Rust (bzw. LLVM unter der Haube) i128 auf einer 64-Bit-Architektur emuliert. Sie erfahren, warum dies nicht so langsam ist, wie es scheint, und wie Sie effizienten Code mit diesen riesigen Zahlen schreiben. Dies ist keine trockene Theorie – wir gehen den Weg von der Frage bis zur praktischen Anwendung anhand von Beispielen.



1. Hardware-Realität: Warum 128 Bit eine Herausforderung sind



Moderne 64-Bit-Prozessoren (x86-64, ARM64) haben Allzweckregister mit einer Größe von 64 Bit. Dies ist ihre „native“ Wortgröße. Um mit einer 128-Bit-Zahl zu arbeiten, muss der Prozessor mehrere Operationen mit zwei 64-Bit-Hälften durchführen: der niedrigen (low) und der hohen (high).



In Rust ist der Typ i128 (und sein vorzeichenloser Bruder u128) kein Hardware-Typ, sondern ein aggregierter Typ. Er wird im Speicher als zwei benachbarte 64-Bit-Wörter gespeichert. Der Compiler (über LLVM) generiert Code, der diese Wörter als eine Einheit verarbeitet.



Ein einfaches Beispiel:



fn main() {    let big: i128 = 123456789012345678901234567890;    println!("{}", big);}


Auf Assembly-Ebene wird dies in eine Sequenz von Anweisungen umgewandelt, die mit zwei 64-Bit-Registern arbeiten (z. B. rax und rdx). LLVM kann solche Operationen sehr effizient „zusammenkleben“.



2. Anatomie der Addition: Wie Rust i128 addiert



Schauen wir uns die grundlegendste Operation an – die Addition zweier i128. Schreiben wir eine einfache Funktion und werfen wir mit cargo-show-asm oder Compiler Explorer einen Blick unter die Haube.



fn add_big(a: i128, b: i128) -> i128 {    a + b}


Kompilieren wir für x86-64 (ohne Optimierungen, zur Veranschaulichung). LLVM wird ungefähr diesen Pseudocode generieren:



  1. Laden der unteren 64 Bit von a in das Register rax, der oberen in rdx.
  2. Laden der unteren 64 Bit von b in rbx, der oberen in rcx.
  3. Addition der unteren Teile: add rax, rbx – Ergebnis in rax, das Übertragsflag (CF) wird gesetzt, falls ein Überlauf auftritt.
  4. Addition der oberen Teile unter Berücksichtigung des Übertrags: adc rdx, rcx – adc (add with carry) addiert 1, wenn das CF-Flag gesetzt war.
  5. Ergebnis: untere 64 Bit in rax, obere in rdx.


Das ist die Emulation. Zwei Anweisungen statt einer. Für Multiplikation und Division wird der Code komplexer, aber das Prinzip bleibt dasselbe – die Aufteilung in 64-Bit-Operationen.



3. Und was ist mit der Leistung? Mythen und Realität



Viele denken, dass i128 doppelt so langsam ist wie i64. Das stimmt nicht ganz. Ja, jede Operation wird in 2-3 Anweisungen statt einer umgewandelt. Aber moderne Prozessoren haben:



  • Pipelines und Superskalarität – sie können mehrere Anweisungen pro Takt ausführen.
  • Sprungsvorhersage – besonders wichtig für Vergleiche.
  • Große Caches – die Arbeit mit dem Speicher leidet nicht, da i128 nur 16 Bytes belegt.


Praktischer Test (Rust, einfache Arithmetik in einer Schleife):



use std::time::Instant;

fn main() { let n = 100_000_000; // i64 let start = Instant::now(); let m

Blogs

Buchempfehlungen