Introducción: por qué Rust ya es rápido, pero no perfecto
Rust se posiciona tradicionalmente como un lenguaje que ofrece el rendimiento de C++ con la seguridad de memoria. Sin embargo, "rápido por defecto" es un mito. Incluso en Rust se puede escribir código lento: clonación excesiva, estructuras de datos no óptimas, uso incorrecto de asignaciones. En este artículo analizaremos métodos prácticos que te ayudarán a exprimir al máximo tu aplicación Rust sin sacrificar la seguridad.
Abordaremos cuatro aspectos clave: perfilado, zero-cost abstractions, gestión de memoria y paralelismo. Cada sección contiene ejemplos de código reales y resultados medibles.
1. Perfilado: no adivines, mide
Sin perfilado, cualquier optimización es disparar a ciegas. Rust tiene varias herramientas potentes.
1.1. Benchmarks integrados con cargo bench
Para microoptimizaciones, usa #![feature(test)] y el módulo test. Ejemplo:
#![feature(test)]extern crate test;
pub fn slow_sum(data: &[u64]) -> u64 { data.iter().sum()}
#[cfg(test)]mod benchmarks { use super::*; use test::Bencher;
#[bench] fn bench_slow_sum(b: &mut Bencher) { let data: Vec<u64> = (0..1000).collect(); b.iter(|| slow_sum(&data)); }}Ejecución: cargo bench. Verás el tiempo exacto de ejecución y el número de iteraciones.
1.2. Perfiladores externos: perf y flamegraph
Para encontrar cuellos de botella en aplicaciones grandes, usa perf en Linux o DTrace en macOS. Instala la herramienta de visualización:
cargo install flamegraphcargo flamegraph --bin your_appEsto crea un gráfico SVG donde cada "lengua de fuego" es una pila de llamadas. Cuanto más ancho es el bloque, más tiempo se gasta en esa función.
Consejo: Siempre perfila en modo release: cargo build --release. El modo debug añade comprobaciones de límites e información de depuración, lo que distorsiona los resultados.
2. Zero-cost abstractions: úsalas, pero con criterio
Rust se enorgullece de que las abstracciones de alto nivel no añadan sobrecarga. Pero esto solo funciona con un uso correcto.
2.1. Iteradores vs bucles
Los iteradores en Rust se compilan al mismo código máquina que los bucles manuales. Ejemplo:
// Mal: bucle manual con índicesfn manual_sum(data: &[u64]) -> u64 { let mut sum = 0; for i in 0..data.len() { sum += data[i]; } sum}
// Bien: iterador (zero-cost)fn iter_sum(data: &[u64]) -> u64 { data.iter().sum()}Los benchmarks muestran un rendimiento idéntico. Pero los iteradores son más seguros (sin comprobación manual de límites) y más legibles.
2.2. Evita asignaciones innecesarias
Cada llamada a Vec::new() o String::new() es una asignación en el heap. Usa arrays en la pila cuando sea posible:
// Mal: asignación en cada llamadafn process_items(items: &[u32]) -> Vec<u32> { items.iter().map(|x| x * 2).collect()}
// Bien: reutilización del bufferfn process_items_inplace(items: &mut [u32]) { for item in items.iter_mut() { *item *= 2; }}Si la asignación es inevitable, usa Vec::with_capacity(n) para evitar múltiples reasignaciones.
3. Gestión de memoria: propiedad y clonación
El sistema de propiedad de Rust es la principal herramienta de optimización. Permite evitar la recolección de basura, pero requiere un enfoque consciente.
3.1. Préstamo vs clonación
Cada .clone() es una copia de datos. Intenta usar referencias:
// Mal: clonación de toda la cadenafn greet(name: String) -> String { format!("Hello, {