Pourquoi Rust n'optimise pas le code avec deux références mutables ? Analyse pour débutants

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 : L'énigme du compilateur Rust



Imaginez : vous écrivez en Rust, vous êtes un véritable fan de la sécurité mémoire. Vous savez que deux références mutables (&mut T) vers les mêmes données ne peuvent jamais exister simultanément. C'est l'une des règles principales d'emprunt (borrowing rules). Le compilateur Rust veille strictement à cela.



Il semblerait que si le compilateur est certain que deux &mut ne se chevauchent pas (ne sont pas des alias), il devrait utiliser cette connaissance pour une optimisation agressive. Par exemple, réorganiser les opérations, mettre en cache les valeurs dans les registres, ne pas relire la mémoire. Mais voici le paradoxe : le compilateur Rust (LLVM) ne le fait souvent pas. Pourquoi ? Cela semble pourtant si logique !



Cette question a recueilli des milliers de vues sur Stack Overflow. Et aujourd'hui, comme de véritables détectives, nous allons comprendre cette énigme. Nous ne nous contenterons pas de lire la réponse — nous allons comprendre les raisons profondes, liées à l'héritage du C, à LLVM et au fonctionnement de l'optimisation. C'est parti !



1. La règle d'or de Rust : Deux &mut sont toujours des données différentes



Rappelons les bases. En Rust, il existe deux types de références :

  • Immuable (&T) : il peut y en avoir autant que l'on veut, mais on ne peut pas les modifier.
  • Mutable (&mut T) : il ne peut y en avoir qu'une seule, mais on peut la modifier.


Cette règle est le noalias pour &mut. Le compilateur, en théorie, peut supposer : "Si je vois deux &mut i32, ils pointent exactement vers des emplacements mémoire différents".



Mais regardons un exemple simple qui illustre le problème :



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


Ici, tout est évident : x et y sont des variables différentes. Rust garantit qu'elles ne se chevauchent pas. Le compilateur pourrait simplement retourner 42, sans effectuer les affectations. Mais est-ce vraiment le cas ?



2. Le problème : LLVM et sa "cécité" face aux garanties de Rust



Le personnage principal de notre histoire est LLVM (Low Level Virtual Machine). Rust utilise LLVM comme backend pour générer du code machine. LLVM est un optimiseur puissant, mais il ne connaît pas les règles de Rust. Il ne connaît que les règles du C/C++.



En C/C++, il existe le mot-clé restrict (en C99), qui indique au compilateur : "Cette référence est unique, personne d'autre ne pointe vers ces données". Mais en C/C++, restrict est une promesse du programmeur, pas une garantie du compilateur. Si le programmeur viole cette promesse — comportement indéfini.



Rust, quant à lui, offre une garantie en béton armé. Mais pour transmettre cette garantie à LLVM, le frontend Rust doit explicitement marquer &mut avec l'attribut noalias dans le IR de LLVM (représentation intermédiaire).



Et c'est là que les choses deviennent intéressantes. Rust marque &mut comme noalias uniquement dans certains cas. Et dans d'autres, non. Pourquoi ?



2.1. Problème historique : UnsafeCell et code Unsafe



À l'intérieur de Rust, il existe le type UnsafeCell<T>. C'est le bloc de construction pour Cell, RefCell, Mutex et d'autres types qui permettent la mutabilité intérieure (interior mutability).



Si vous avez un &mut UnsafeCell<i32>, vous pouvez obtenir un pointeur brut et modifier les données même via une référence immuable. Cela brise la garantie noalias !



Pour éviter un comportement indéfini, le compilateur Rust ne place pas noalias sur &mut si un UnsafeCell peut se cacher derrière. Mais le problème est plus profond.



3. Exemple concret : pourquoi l'optimisation ne fonctionne pas



Revenons à la fonction example

Blogs

Book Recommendations