Por qué Rust no optimiza el código con dos referencias mutables? Explicación para principiantes

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Introducción: El enigma del compilador Rust



Imagina: escribes en Rust, eres un verdadero fanático de la seguridad de memoria. Sabes que dos mutable reference (&mut T) a los mismos datos nunca pueden existir simultáneamente. Esta es una de las reglas principales de préstamo (borrowing rules). El compilador de Rust lo verifica estrictamente.



Parecería que si el compilador está seguro de que dos &mut no se superponen (no son alias), debería usar este conocimiento para una optimización agresiva. Por ejemplo, reordenar operaciones, almacenar valores en caché en registros, no volver a leer memoria. Pero aquí está la paradoja: el compilador de Rust (LLVM) a menudo no lo hace. ¿Por qué? ¡Parece tan lógico!



Esta pregunta ha acumulado miles de visitas en Stack Overflow. Y hoy, como verdaderos detectives, resolveremos este enigma. No solo leeremos la respuesta, sino que entenderemos las razones profundas, relacionadas con la herencia de C, LLVM y cómo funciona la optimización. ¡Allá vamos!



1. La regla de oro de Rust: Dos &mut son siempre datos diferentes



Recordemos lo básico. En Rust hay dos tipos de referencias:

  • Inmutable (&T): puede haber muchas, pero no se pueden modificar.
  • Mutable (&mut T): solo puede haber una, pero se puede modificar.


Esta regla es noalias para &mut. El compilador, en teoría, puede suponer: "Si veo dos &mut i32, apuntan con certeza a diferentes lugares en memoria".



Pero veamos un ejemplo simple que demuestra el problema:



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


Aquí todo es obvio: x e y son variables diferentes. Rust garantiza que no se superponen. El compilador podría simplemente devolver 42, sin realizar las asignaciones. Pero, ¿es realmente así?



2. El problema: LLVM y su "ceguera" a las garantías de Rust



El protagonista de nuestra historia es LLVM (Low Level Virtual Machine). Rust utiliza LLVM como backend para generar código máquina. LLVM es un potente optimizador, pero no conoce las reglas de Rust. Solo conoce las reglas de C/C++.



En C/C++ existe la palabra clave restrict (en C99), que le dice al compilador: "Esta referencia es única, nadie más apunta a estos datos". Pero en C/C++, restrict es una promesa del programador, no una garantía del compilador. Si el programador rompe la promesa, hay comportamiento indefinido (undefined behavior).



Rust, en cambio, ofrece una garantía de hierro. Pero para transmitir esta garantía a LLVM, el frontend de Rust debe marcar explícitamente &mut con el atributo noalias en el IR de LLVM (representación intermedia).



Y aquí es donde comienza lo interesante. Rust marca &mut como noalias solo en ciertos casos. Y en otros, no. ¿Por qué?



2.1. Problema histórico: UnsafeCell y código Unsafe



Dentro de Rust existe el tipo UnsafeCell<T>. Este es el bloque de construcción para Cell, RefCell, Mutex y otros tipos que permiten mutabilidad interior (interior mutability).



Si tienes un &mut UnsafeCell<i32>, a través de él puedes obtener un puntero crudo y modificar los datos incluso a través de una referencia inmutable. ¡Esto rompe la garantía de noalias!



Para evitar comportamiento indefinido, el compilador de Rust no coloca noalias en &mut si detrás puede haber un UnsafeCell. Pero el problema es más profundo.



3. Ejemplo real: por qué la optimización no funciona



Volvamos a la función example

Blogs

Book Recommendations