Perfilado de código Rust: encontramos cuellos de botella y aceleramos programas

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: ¿Por qué perfilar código Rust?



Rust es famoso por su velocidad y seguridad, pero incluso el código más rápido puede ejecutarse lentamente debido a cuellos de botella no evidentes. El perfilado es el proceso de medir el tiempo de ejecución, el uso de memoria y otros recursos, lo que ayuda a encontrar "cuellos de botella". Sin perfilado, la optimización se convierte en adivinanzas: podrías "mejorar" secciones de código que no afectan en absoluto el rendimiento.



En este artículo, cubriremos las principales herramientas de perfilado de Rust: perf, flamegraph, cargo-flamegraph y el benchmarking integrado. Aprenderás cómo encontrar funciones lentas, asignaciones y fallos de caché, así como interpretar los datos obtenidos.



Herramientas de Perfilado de Rust: Eligiendo el Enfoque Correcto



1. Benchmarking Integrado con #[bench]



El primer paso para perfilar es escribir benchmarks. Rust tiene soporte integrado para benchmarks (inestable, requiere nightly). Ejemplo:



// En el archivo benches/my_bench.rs#![feature(test)]extern crate test;

use test::Bencher;

#[bench]fn bench_my_function(b: &mut Bencher) { let data = vec![1, 2, 3, 4, 5]; b.iter(|| { // Función bajo prueba let _ = data.iter().sum::<i32>(); });}


Ejecuta: cargo bench. Obtendrás mediciones precisas de tiempo, pero esto no mostrará dónde se gasta el tiempo dentro de la función.



2. perf — Perfilador a Nivel de Kernel



La herramienta perf (Linux) recopila muestras de contadores de CPU. Muestra qué líneas de código se ejecutan con más frecuencia.



Instalación: sudo apt install linux-tools-common linux-tools-generic.



Ejemplo de uso:



# Compilar con símbolos de depuracióncargo build --release

# Ejecutar perfiladosudo perf record ./target/release/my_program

# Ver el informesudo perf report


En el informe, verás funciones ordenadas por porcentaje de tiempo de ejecución. Estos son los principales candidatos para optimización.



3. flamegraph — Visualización de la Pila de Llamadas



Flamegraph es un diagrama SVG donde el ancho de cada rectángulo corresponde al tiempo de ejecución de la función. Cuanto más ancho es el rectángulo, más tiempo se gasta en esa función o sus llamadas hijas.



Instalando cargo-flamegraph:



cargo install flamegraph


Ejecutando:



cargo flamegraph --bin my_program


El resultado es un archivo flamegraph.svg. Ábrelo en tu navegador. Busca "mesetas" anchas — esos son los cuellos de botella.



Ejemplo Práctico: Encontrando un Cuello de Botella en Código Rust



Consideremos un programa simple que procesa un gran arreglo de datos:



fn process(data: &[u8]) -> Vec<u8> {    let mut result = Vec::with_capacity(data.len());    for &byte in data {        result.push(byte.wrapping_mul(2));    }    result}

fn main() { let data = vec![0u8; 10_000_000]; let _ = process(&data);}


Compila y ejecuta el perfilado:



cargo build --releasecargo flamegraph --bin my_program


Al abrir el flamegraph, vemos que la mayor parte del tiempo se gasta en el bucle for y en la llamada a wrapping_mul. Ten en cuenta que Vec::push puede causar reasignaciones si la capacidad no está calculada con precisión. En nuestro caso, with_capacity está configurada correctamente, pero el bucle en sí es el cuello de botella.



Optimización: usar iteradores y map:



fn process_optimized(data: &[u8]) -> Vec<u8> {    data.iter().map(|&b| b.wrapping_mul(2)).collect()}


Volver a medir muestra una mejora de velocidad del 10-15% debido a la vectorización (auto-vectorización de LLVM).



Técnicas Avanzadas: Al

Blogs

Book Recommendations