Introducción: por qué el rendimiento de Rust no es magia, sino disciplina
Rust es, con razón, considerado uno de los lenguajes de programación de sistemas más rápidos. Sin embargo, como en cualquier otro lenguaje, el código "crudo" no siempre funciona al máximo de sus capacidades. Muchos desarrolladores principiantes, al migrar a Rust desde Python o JavaScript, esperan un aumento instantáneo de velocidad, pero olvidan que la optimización es un proceso, no un regalo del compilador.
Este artículo está dirigido a quienes ya han escrito sus primeros programas en Rust y quieren entender cómo sacarles el máximo partido. Abordaremos tres áreas clave: perfilado (para saber qué corregir), abstracciones de costo cero (para no pagar por lo que no usas) y gestión de memoria (para evitar cuellos de botella). Todos los ejemplos irán acompañados de código que puedes ejecutar inmediatamente en tu entorno.
1. Perfilado: antes de optimizar, mide
La regla principal de la optimización: no optimices a ciegas. Sin perfilado, corres el riesgo de pasar horas "acelerando" código que ya funciona rápido, mientras que el verdadero cuello de botella pasa desapercibido.
Herramientas para perfilado
En el ecosistema de Rust hay varias herramientas excelentes. Para empezar, recomiendo cargo-flamegraph — visualiza dónde pasa más tiempo el programa. Instalación:
cargo install flamegraphEjecución del perfilado para tu archivo binario:
cargo flamegraph --bin my_appTras la ejecución, obtendrás un archivo SVG con el "flame graph". Las "barras" anchas muestran las funciones que consumen más CPU.
Ejemplo: encontramos el cuello de botella
Supongamos que tenemos una función que suma los cuadrados de números:
fn sum_of_squares(n: u64) -> u64 { let mut sum = 0; for i in 1..=n { sum += i * i; } sum}Si el perfilador muestra que el 90% del tiempo se gasta en esta función, sabemos dónde mirar. Si el cuello de botella es la entrada/salida (por ejemplo, lectura de archivos), optimizar los cálculos no dará resultado.
2. Abstracciones de costo cero: cómo ayuda (y estorba) el compilador
Una de las principales características de Rust es el concepto de abstracciones de costo cero. Esto significa que las construcciones de alto nivel (iteradores, closures, traits) no deben añadir sobrecarga en comparación con la escritura manual de bucles. Sin embargo, esto solo funciona con un uso correcto.
Iteradores vs. bucles
Muchos principiantes escriben así:
let v: Vec<i32> = (0..1000).collect();let mut result = Vec::new();for i in &v { result.push(i * 2);}Este código se puede reescribir con iteradores, y el compilador de Rust lo optimizará al mismo código máquina:
let v: Vec<i32> = (0..1000).collect();let result: Vec<i32> = v.iter().map(|x| x * 2).collect();Más aún, si usas filter, map y fold secuencialmente, el compilador puede "fusionarlos" en una sola pasada — esto se llama loop fusion. Los bucles manuales no permiten esta optimización.
Box<dyn Trait> vs. genéricos
Otro error común es abusar de los trait objects (Box<dyn SomeTrait>). Estos provocan despacho dinámico, lo que añade sobrecarga. Si la cantidad de variantes se conoce en tiempo de compilación, usa genéricos (despacho estático):
// Malo: despacho dinámicofn process(items: Vec<Box<dyn Processor>>) { for item in items { item.run(); }}
// Bueno: despacho estáticofn process<T: Processor>(items: Vec<T>) { for item in items { item.run()