Profiling y Benchmarking con Criterion

Lectura
20 min~4 min lectura

Concepto clave

En sistemas de baja latencia y alta seguridad, cada microsegundo cuenta y cada operación debe ser predecible. El profiling es el proceso de medir el rendimiento de tu código para identificar cuellos de botella, mientras que el benchmarking compara diferentes implementaciones bajo condiciones controladas. Imagina que eres un ingeniero de Fórmula 1: el profiling es como analizar los datos de telemetría para ver dónde el coche pierde tiempo en la pista, y el benchmarking es probar diferentes configuraciones de alerones en el túnel de viento para elegir la más rápida.

Criterion es la herramienta estándar en Rust para benchmarking estadístico. A diferencia de mediciones simples con std::time::Instant, Criterion ejecuta múltiples iteraciones, aplica análisis estadístico para eliminar ruido, y genera informes visuales. Esto es crucial en sistemas críticos donde variaciones de nanosegundos pueden afectar SLA (Service Level Agreements) o crear vulnerabilidades de timing.

Cómo funciona en la práctica

Primero, añade Criterion a tu Cargo.toml en la sección dev-dependencies:

[dev-dependencies]
criterion = "0.5"

Crea un archivo benches/my_benchmark.rs en tu proyecto. Aquí un ejemplo que compara dos algoritmos de hash para seguridad:

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use sha2::{Sha256, Digest};
use blake3::Hasher;

fn bench_sha256(c: &mut Criterion) {
    let data = vec![0u8; 1024]; // 1KB de datos
    c.bench_function("sha256_1kb", |b| {
        b.iter(|| {
            let mut hasher = Sha256::new();
            hasher.update(black_box(&data));
            hasher.finalize()
        })
    });
}

fn bench_blake3(c: &mut Criterion) {
    let data = vec![0u8; 1024];
    c.bench_function("blake3_1kb", |b| {
        b.iter(|| {
            let mut hasher = Hasher::new();
            hasher.update(black_box(&data));
            hasher.finalize()
        })
    });
}

criterion_group!(benches, bench_sha256, bench_blake3);
criterion_main!(benches);

Ejecuta con cargo bench. Criterion ejecutará cada función miles de veces, calculará estadísticas como media, desviación estándar y outliers, y generará un informe en target/criterion/report/index.html. Usa black_box para evitar que el compilador optimice código que no debería, simulando una entrada real.

Caso de estudio

En un sistema de trading de alta frecuencia, necesitas validar firmas digitales en menos de 10 microsegundos. El equipo probó dos bibliotecas: ring (de BoringSSL) y rustls. Configuraron Criterion para medir la verificación de firmas ECDSA P-256 con datos de 256 bytes.

BibliotecaTiempo medio (ns)Desviación estándar (ns)Outliers
ring8,4501200.1%
rustls9,2003500.5%
La baja desviación estándar de ring indica mayor predictibilidad, crucial para baja latencia. Aunque la diferencia media es solo 750ns, en 10,000 operaciones/segundo, ring ahorra 7.5ms por segundo, reduciendo colas y mejorando throughput.

El informe de Criterion mostró que ring usaba ensamblador optimizado, mientras rustls tenía más branches. Decidieron usar ring para firmas críticas y rustls para conexiones TLS menos sensibles.

Errores comunes

  • No aislar el código benchmarked: Medir operaciones que incluyen E/S o allocations del sistema. Usa black_box y prepara datos en el setup.
  • Ignorar el ruido del sistema: Ejecutar benchmarks en máquinas con carga variable. Usa máquinas dedicadas o tools como taskset para fijar CPUs.
  • Comparar sin contexto estadístico: Decidir basado en una sola ejecución. Criterion proporciona intervalos de confianza; si se solapan, las diferencias pueden no ser significativas.
  • Olvidar el overhead de medición: En operaciones de nanosegundos, el loop de benchmarking añade overhead. Usa Bencher::iter_custom para mediciones muy finas.
  • No validar seguridad: Optimizar a costa de constant-time operations. En criptografía, verifica que los benchmarks no introduzcan side-channels.

Checklist de dominio

  1. Configurar Criterion en un proyecto Rust con benchmarks aislados.
  2. Interpretar reportes HTML: media, desviación, outliers, y comparaciones.
  3. Usar black_box correctamente para evitar optimizaciones no deseadas.
  4. Diseñar benchmarks que reflejen cargas reales de sistemas de baja latencia.
  5. Integrar benchmarking en CI/CD para detectar regresiones de performance.
  6. Distinguir entre variabilidad aceptable y problemas de performance.
  7. Combinar profiling (con tools como perf o flamegraph) y benchmarking para optimización completa.

Benchmark de un Cache LRU para Baja Latencia

Implementa y compara dos implementaciones de cache LRU (Least Recently Used) para un sistema de API que requiere respuestas en menos de 1ms.

  1. Crea un nuevo proyecto Rust con cargo new lru_benchmark.
  2. Añade estas dependencias a Cargo.toml:
    [dependencies]
    lru = "0.11"
    
    [dev-dependencies]
    criterion = "0.5"
  3. En src/lib.rs, implementa una versión simple de LRU usando std::collections::VecDeque y std::collections::HashMap con capacidad fija de 1000 elementos.
  4. En benches/cache_bench.rs, crea un benchmark con Criterion que compare:
    • Tu implementación custom de LRU.
    • La crate lru con la misma capacidad.
    Mide el tiempo para insertar 1000 elementos y luego acceder a 100 elementos aleatorios 100 veces.
  5. Ejecuta cargo bench y analiza el reporte en target/criterion.
  6. Documenta tus hallazgos: ¿cuál es más rápido? ¿Cuál tiene menor desviación estándar? ¿Cuál elegirías para baja latencia?
Pistas
  • Usa black_box en los datos de acceso aleatorio para simular entrada real.
  • Configura el LRU con capacidad fija para evitar allocations durante el benchmark.
  • Considera usar criterion_group con BenchmarkId para comparaciones claras.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.