Le secret d'i128 sur les systèmes 64 bits : comment Rust gère les nombres géants

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Introduction : Les grands nombres dans un petit monde



Imaginez : vous écrivez un algorithme cryptographique, une simulation de processus physiques ou vous calculez simplement la distance jusqu'à la galaxie la plus proche en nanomètres. Et soudain, vous réalisez que le i64 standard (de -9 quintillions à 9 quintillions) est catastrophiquement insuffisant. Rust a une bouée de sauvetage : le type i128. Mais comment fonctionne-t-il sur un processeur qui ne peut physiquement pas gérer des nombres 128 bits ? Est-ce de la magie ?



Indice : il n'y a pas de magie, il y a une ingénierie géniale. Dans cet article, nous allons voir comment Rust (ou plus précisément, LLVM sous le capot) émule i128 sur une architecture 64 bits. Vous découvrirez pourquoi ce n'est pas aussi lent qu'il n'y paraît et comment écrire du code efficace avec ces nombres géants. Ce n'est pas de la théorie sèche — nous passerons de la question à l'application réelle avec des exemples.



1. La réalité matérielle : pourquoi 128 bits est un défi



Les processeurs 64 bits modernes (x86-64, ARM64) ont des registres à usage général de 64 bits. C'est leur taille de mot « native ». Pour travailler avec un nombre 128 bits, le processeur doit effectuer plusieurs opérations sur deux moitiés 64 bits : la partie basse (low) et la partie haute (high).



Dans Rust, le type i128 (et son homologue non signé u128) n'est pas un type matériel, mais un type agrégé. Il est stocké en mémoire comme deux mots 64 bits contigus. Le compilateur (via LLVM) génère du code qui traite ces mots comme un tout.



Un exemple simple :



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


Au niveau assembleur, cela se transforme en une séquence d'instructions travaillant avec deux registres 64 bits (par exemple, rax et rdx). LLVM sait très efficacement « coller » ces opérations.



2. Anatomie de l'addition : comment Rust additionne i128



Regardons l'opération la plus basique — l'addition de deux i128. Écrivons une fonction simple et regardons sous le capot avec cargo-show-asm ou Compiler Explorer.



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


Compilons pour x86-64 (sans optimisations, pour plus de clarté). LLVM générera approximativement ce pseudo-code :



  1. Charger les 64 bits bas de a dans le registre rax, les bits hauts dans rdx.
  2. Charger les 64 bits bas de b dans rbx, les bits hauts dans rcx.
  3. Additionner les parties basses : add rax, rbx — résultat dans rax, le drapeau de retenue (CF) est défini en cas de débordement.
  4. Additionner les parties hautes avec retenue : adc rdx, rcx — adc (add with carry) ajoute 1 si le drapeau CF était défini.
  5. Résultat : les 64 bits bas dans rax, les bits hauts dans rdx.


C'est cela, l'émulation. Deux instructions au lieu d'une. Pour la multiplication et la division, le code sera plus complexe, mais le principe reste le même — la décomposition en opérations 64 bits.



3. Et les performances ? Mythes et réalité



Beaucoup pensent que i128 est deux fois plus lent que i64. Ce n'est pas tout à fait vrai. Oui, chaque opération se transforme en 2-3 instructions au lieu d'une. Mais les processeurs modernes ont :



  • Des pipelines et la superscalarité — ils peuvent exécuter plusieurs instructions par cycle.
  • La prédiction de branchement — particulièrement importante pour les comparaisons.
  • De grands caches — le travail avec la mémoire n'en souffre pas, car i128 ne prend que 16 octets.


Test pratique (Rust, arithmétique simple en boucle) :



use std::time::Instant;

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

Blogs

Book Recommendations