Medir cuánto tarda un trozo de código en Rust parece trivial: Instant::now(), el cálculo, elapsed(). El problema es que la primera medición casi siempre miente, y conviene saber por qué antes de sacar conclusiones.
También te puede interesar
Medir, en principio
La herramienta está en la biblioteca estándar y no necesita dependencias:
use std::hint::black_box;
use std::time::Instant;
fn es_primo(n: u64) -> bool {
if n < 2 { return false; }
let mut d = 2;
while d * d <= n {
if n % d == 0 { return false; }
d += 1;
}
true
}
fn main() {
let limite = black_box(200_000u64);
let t = Instant::now();
let primos = black_box((2..limite).filter(|&n| es_primo(n)).count());
let tardado = t.elapsed();
println!("compilado en: {}", if cfg!(debug_assertions) { "depuración" } else { "release" });
println!("primos hasta {limite}: {primos}");
println!("tardó: {:.2?}", tardado);
}
cargo runcompilado en: depuración primos hasta 200000: 17984 tardó: 21.52ms
cargo run --releasecompilado en: release primos hasta 200000: 17984 tardó: 14.89ms
Repetido cinco veces cada uno, la depuración se mueve entre 20,87 y 21,79 ms, y el release entre 14,76 y 16,67. Es decir, unas 1,4 veces más rápido, no las diez o cincuenta que se suelen citar. En este cálculo manda la división entera, que el optimizador no puede acelerar mucho. La diferencia entre depuración y release depende enteramente de lo que haga tu código.
Por qué está ese black_box
Esta es la parte importante, y la descubrí equivocándome. La primera versión de este ejemplo sumaba cuadrados en vez de contar primos, y en release daba este resultado:
sumando cuadrados hasta 3.000.000, en releaseresultado : 9000004500000500000 sin black_box: 120.00ns con black_box: 40.00ns
Cuarenta nanosegundos para tres millones de iteraciones es imposible: no da tiempo ni a recorrer la memoria. Lo que ocurrió es que el optimizador reconoció la suma de cuadrados y la sustituyó por la fórmula cerrada. El resultado es correcto —coincide exactamente con n(n+1)(2n+1)/6— pero el bucle nunca se ejecutó, así que no había nada que medir.
black_box sirve para impedir ese tipo de optimización: le dice al compilador que no puede suponer nada sobre ese valor. Aun así no es infalible, como demuestra ese caso, donde ni con la barrera se evitó la sustitución por la fórmula. Si una medición da un número absurdamente bueno, la hipótesis correcta no es que tu código sea rapidísimo, sino que no se ejecutó.
u64. En depuración el programa moría con attempt to add with overflow; en release no se quejaba y devolvía 13919798230507451072, un número sencillamente falso. Es la diferencia entre los dos modos que explica la lección de tipos, vista en vivo: comprobar en depuración, dar la vuelta en release.Reglas para medir sin engañarte
| Regla | Por qué |
|---|---|
Siempre con --release |
En depuración mides las comprobaciones, no tu código |
| Usa el resultado | Si nadie lo mira, el compilador borra el cálculo |
Envuelve entradas y salidas en black_box |
Evita que se precalculen en tiempo de compilación |
| Repite varias veces | Una sola medida recoge el ruido de la máquina |
| Desconfía de lo imposible | Un millón de iteraciones no caben en nanosegundos |
| Para medir en serio, usa criterion | Repite, descarta valores atípicos y da intervalos |
Instant frente a las fechas. Instant es un reloj monótono: solo sirve para restar dos lecturas y nunca retrocede, aunque alguien cambie la hora del sistema. Por eso se usa para medir y no para registrar cuándo pasó algo. Para eso están las fechas con chrono, que es la distinción que hace Rust entre medir intervalos y manejar calendarios.El código de esta página se compiló y ejecutó con rustc 1.96.1 antes de publicarla, en los dos modos y con cinco repeticiones de cada uno; todos los números están copiados de esas ejecuciones. Documentación oficial: std::time::Instant y std::hint::black_box.