Cómo hacer benchmark del código Rust con Iai


¿Qué es la Evaluación Comparativa?

La evaluación comparativa es la práctica de probar el rendimiento de tu código para ver qué tan rápido (latencia) o cuánto (rendimiento) trabajo puede hacer. Este paso, a menudo pasado por alto en el desarrollo de software, es crucial para crear y mantener un código rápido y de alto rendimiento. La evaluación comparativa proporciona las métricas necesarias para que los desarrolladores comprendan cómo se comporta su código bajo diversas cargas de trabajo y condiciones. Por las mismas razones por las cuales escribes pruebas unitarias y de integración para prevenir regresiones de características, debes escribir evaluaciones comparativas para prevenir regresiones de rendimiento. ¡Los errores de rendimiento son errores!

Escribe FizzBuzz en Rust

Para escribir evaluaciones comparativas, necesitamos algún código fuente para comparar. Para empezar, vamos a escribir un programa muy simple, FizzBuzz.

Las reglas para FizzBuzz son las siguientes:

Escribe un programa que imprima los números enteros del 1 al 100 (incluyendo ambos):

  • Para múltiplos de tres, imprime Fizz
  • Para múltiplos de cinco, imprime Buzz
  • Para múltiplos de ambos, tres y cinco, imprime FizzBuzz
  • Para todos los demás, imprime el número

Hay muchas formas de escribir FizzBuzz. Así que vamos a elegir mi favorita:

fn main() {
for i in 1..=100 {
match (i % 3, i % 5) {
(0, 0) => println!("FizzBuzz"),
(0, _) => println!("Fizz"),
(_, 0) => println!("Buzz"),
(_, _) => println!("{i}"),
}
}
}
  • Crea una función main
  • Itera desde 1 hasta 100 de manera inclusiva.
  • Para cada número, calcula el módulo (resto después de la división) tanto para 3 como para 5.
  • Coincide el patrón con los dos restos. Si el resto es 0, entonces el número es múltiplo del factor dado.
  • Si el resto es 0 tanto para 3 como para 5, imprime FizzBuzz.
  • Si el resto es 0 solo para 3, entonces imprime Fizz.
  • Si el resto es 0 solo para 5, entonces imprime Buzz.
  • De lo contrario, simplemente imprime el número.

Sigue Paso a Paso

Para seguir este tutorial paso a paso, necesitarás instalar Rust.

🐰 El código fuente de esta publicación está disponible en GitHub

Con Rust instalado, puedes abrir una ventana de terminal e introducir: cargo init game

Luego navega hacia el directorio game recién creado.

game
├── Cargo.toml
└── src
└── main.rs

Deberías ver un directorio llamado src con un archivo llamado main.rs:

fn main() {
println!("Hello, world!");
}

Reemplaza sus contenidos con la implementación de FizzBuzz de arriba. Luego ejecuta cargo run. La salida debería verse así:

$ cargo run
Compiling playground v0.0.1 (/home/bencher)
Finished dev [unoptimized + debuginfo] target(s) in 0.44s
Running `target/debug/game`
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
97
98
Fizz
Buzz

🐰 ¡Boom! ¡Estás rompiendo la entrevista de codificación!

Se debería haber generado un nuevo archivo Cargo.lock:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs

Antes de avanzar más, es importante discutir las diferencias entre micro-benchmarking y macro-benchmarking.

Micro-Benchmarking vs Macro-Benchmarking

Existen dos categorías principales de benchmarks de software: micro-benchmarks y macro-benchmarks. Los micro-benchmarks operan a un nivel similar a las pruebas unitarias. Por ejemplo, un benchmark para una función que determina Fizz, Buzz, o FizzBuzz para un único número sería un micro-benchmark. Los macro-benchmarks operan a un nivel similar a las pruebas de integración. Por ejemplo, un benchmark para una función que juega el juego completo de FizzBuzz, desde 1 hasta 100, sería un macro-benchmark.

Generalmente, es mejor probar al nivel más bajo de abstracción posible. En el caso de los benchmarks, esto los hace más fáciles de mantener, y ayuda a reducir la cantidad de ruido en las mediciones. Sin embargo, al igual que tener algunas pruebas de extremo a extremo puede ser muy útil para verificar la cordura todo el sistema se junta como se esperaba, tener macro-benchmarks puede ser muy útil para asegurarse de que los caminos críticos a través de su software se mantienen con buen rendimiento.

Evaluación de rendimiento en Rust

Las tres opciones populares para la evaluación de rendimiento en Rust son: libtest bench, Criterion, e Iai.

libtest es el marco de evaluación de pruebas unitarias y rendimiento integrado de Rust. Aunque forma parte de la biblioteca estándar de Rust, libtest bench todavía se considera inestable, por lo que solo está disponible en versiones del compilador nightly. Para trabajar en el compilador estable de Rust, se necesita un arnés de evaluación de rendimiento separado. Sin embargo, ninguno está siendo desarrollado activamente.

El arnés de evaluación de rendimiento más popular dentro del ecosistema de Rust es Criterion. Funciona tanto en versiones del compilador Rust estable como nightly, y se ha convertido en el estándar de facto dentro de la comunidad Rust. Criterion también es mucho más rico en funcionalidades en comparación con libtest bench.

Una alternativa experimental a Criterion es Iai, del mismo creador de Criterion. Sin embargo, utiliza el conteo de instrucciones en lugar del tiempo de pared: instrucciones de CPU, accesos a L1, accesos a L2 y accesos a RAM. Esto permite evaluaciones de rendimiento de una sola ejecución, ya que estas métricas deberían mantenerse casi idénticas entre ejecuciones.

Los tres son respaldados por Bencher. Entonces, ¿por qué elegir Iai? Iai utiliza recuentos de instrucciones en lugar de tiempo de reloj de pared. Esto lo hace ideal para benchmarking continuo, es decir, benchmarking en CI. Sugeriría usar Iai para benchmarking continuo, especialmente si estás usando runners compartidos. Es importante entender que Iai solo mide un sustituto de lo que realmente te interesa. ¿Ir de 1,000 instrucciones a 2,000 instrucciones duplica la latencia de tu aplicación? Quizás sí, quizás no. Por esa razón, puede ser útil también ejecutar benchmarks basados en tiempo de reloj de pared en paralelo con benchmarks basados en recuento de instrucciones.

🐰 Iai no ha sido actualizado en más de 3 años. Por lo tanto, podrías considerar usar Iai-Callgrind en su lugar.

Instalar Valgrind

Iai utiliza una herramienta llamada Valgrind para recoger recuentos de instrucciones. Valgrind es compatible con Linux, Solaris, FreeBSD, y MacOS. Sin embargo, el soporte de MacOS está limitado a procesadores x86_64 ya que los procesadores arm64 (M1, M2, etc) aún no son compatibles.

En Debian ejecuta: sudo apt-get install valgrind

En MacOS (chip x86_64/Intel solamente): brew install valgrind

Refactorizar FizzBuzz

Para probar nuestra aplicación FizzBuzz, necesitamos desacoplar nuestra lógica de la función main del programa. Los arneses de benchmark no pueden marcar la función main. Para hacer esto, necesitamos hacer algunos cambios.

Bajo src, crea un nuevo archivo llamado lib.rs:

game
├── Cargo.lock
├── Cargo.toml
└── src
└── lib.rs
└── main.rs

Agrega el siguiente código a lib.rs:

pub fn play_game(n: u32, print: bool) {
let result = fizz_buzz(n);
if print {
println!("{result}");
}
}
pub fn fizz_buzz(n: u32) -> String {
match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
(_, _) => n.to_string(),
}
}
  • play_game: Recibe un número entero sin signo n, llama a fizz_buzz con ese número, y si print es true imprime el resultado.
  • fizz_buzz: Recibe un número entero sin signo n y realiza la lógica real de Fizz, Buzz, FizzBuzz, o número regresando el resultado como string.

Luego actualiza main.rs para que se vea así:

use game::play_game;
fn main() {
for i in 1..=100 {
play_game(i, true);
}
}
  • game::play_game: Importa play_game del crate game que acabamos de crear con lib.rs.
  • main: El punto de entrada principal a nuestro programa que itera a través de los números del 1 al 100 inclusive y llama play_game para cada número, con print establecido a true.

Haciendo benchmark de FizzBuzz

Para hacer benchmark de nuestro código, necesitamos crear un directorio benches y agregar un archivo para contener nuestros benchmarks, play_game.rs:

game
├── Cargo.lock
├── Cargo.toml
└── benches
└── play_game.rs
└── src
└── lib.rs
└── main.rs

Dentro de play_game.rs agrega el siguiente código:

use game::play_game;
fn bench_play_game() {
iai::black_box(for i in 1..=100 {
play_game(i, false)
});
}
iai::main!(bench_play_game);
  • Importa la función play_game de nuestro paquete game.
  • Crea una función llamada bench_play_game.
  • Ejecuta nuestro macro-benchmark dentro de una “caja negra” para que el compilador no optimice nuestro código.
  • Itera desde 1 hasta 100 de forma inclusiva.
  • Para cada número, llama play_game, con print establecido en false.

Ahora necesitamos configurar el paquete game para ejecutar nuestros benchmarks.

Añade lo siguiente al final de tu archivo Cargo.toml:

[dev-dependencies]
iai = "0.1"
[[bench]]
name = "play_game"
harness = false
  • iai: Añade iai como una dependencia de desarrollo, ya que solo la estamos utilizando para las pruebas de rendimiento.
  • bench: Registra play_game como benchmark y establece harness en false, ya que utilizaremos Iai como nuestro cabrestante de benchmark.

Ahora estamos listos para hacer benchmark de nuestro código, ejecuta cargo bench:

$ cargo bench
Compiling iai v0.1.1
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 2.55s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 34370
L1 Accesses: 50373
L2 Accesses: 9
RAM Accesses: 35
Estimated Cycles: 51643

🐰 ¡Vamos a subir la remolacha! ¡Tenemos nuestras primeras métricas de benchmark!

Finalmente, podemos descansar nuestras cansadas cabezas de desarrolladores… Solo bromeaba, ¡nuestros usuarios quieren una nueva función!

Escribe FizzBuzzFibonacci en Rust

Nuestros Indicadores Clave de Desempeño (KPIs) están bajos, por lo que nuestro Gerente de Producto (PM) quiere que agreguemos una nueva función. Después de mucho lluvia de ideas y muchas entrevistas con usuarios, se decidió que el FizzBuzz de siempre no es suficiente. Los niños de hoy en día quieren un nuevo juego, FizzBuzzFibonacci.

Las reglas para FizzBuzzFibonacci son las siguientes:

Escribe un programa que imprima los enteros del 1 al 100 (inclusive):

  • Para múltiplos de tres, imprime Fizz
  • Para múltiplos de cinco, imprime Buzz
  • Para múltiplos de tres y cinco, imprime FizzBuzz
  • Para números que sean parte de la secuencia de Fibonacci, solo imprime Fibonacci
  • Para todos los demás, imprime el número

La secuencia de Fibonacci es una serie en la que cada número es la suma de los dos números anteriores. Por ejemplo, comenzando con 0 y 1 el siguiente número en la secuencia de Fibonacci sería 1. Seguido de: 2, 3, 5, 8 y así sucesivamente. Los números que forman parte de la secuencia de Fibonacci se conocen como números de Fibonacci. Por lo tanto, tendremos que escribir una función que detecte los números de Fibonacci.

Hay muchas formas de escribir la secuencia de Fibonacci y de igual forma muchas maneras de detectar un número de Fibonacci. Así que elegiremos mi forma favorita:

fn is_fibonacci_number(n: u32) -> bool {
for i in 0..=n {
let (mut previous, mut current) = (0, 1);
while current < i {
let next = previous + current;
previous = current;
current = next;
}
if current == n {
return true;
}
}
false
}
  • Crea una función llamada is_fibonacci_number que toma un número entero sin signo y devuelve un booleano.
  • Itera para todos los números desde 0 hasta nuestro número dado n inclusive.
  • Inicializa nuestra secuencia Fibonacci comenzando con 0 y 1 como los números anterior y actual respectivamente.
  • Itera mientras el número actual sea menor que la iteración actual i.
  • Suma el número anterior y el número actual para obtener el número siguiente.
  • Actualiza el número anterior al número actual.
  • Actualiza el número actual al número siguiente.
  • Una vez que actual sea mayor o igual al número dado n, saldremos del bucle.
  • Verifica si el número actual es igual al número dado n y si es así, devuelve true.
  • De lo contrario, devuelve false.

Ahora necesitaremos actualizar nuestra función fizz_buzz:

pub fn fizz_buzz_fibonacci(n: u32) -> String {
if is_fibonacci_number(n) {
"Fibonacci".to_string()
} else {
match (n % 3, n % 5) {
(0, 0) => "FizzBuzz".to_string(),
(0, _) => "Fizz".to_string(),
(_, 0) => "Buzz".to_string(),
(_, _) => n.to_string(),
}
}
}
  • Renombra la función fizz_buzz a fizz_buzz_fibonacci para que sea más descriptiva.
  • Llama a nuestra función auxiliar is_fibonacci_number.
  • Si el resultado de is_fibonacci_number es true, entonces devuelve Fibonacci.
  • Si el resultado de is_fibonacci_number es false, entonces realiza la misma lógica de Fizz, Buzz, FizzBuzz o número devolviendo el resultado.

Debido a que renombramos fizz_buzz a fizz_buzz_fibonacci también necesitamos actualizar nuestra función play_game:

pub fn play_game(n: u32, print: bool) {
let result = fizz_buzz_fibonacci(n);
if print {
println!("{result}");
}
}

Tanto nuestras funciones main como bench_play_game pueden permanecer exactamente iguales.

Haciendo benchmark de FizzBuzzFibonacci

Ahora podemos volver a ejecutar nuestro benchmark:

$ cargo bench
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 2.20s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 304598 (+786.2322%)
L1 Accesses: 320024 (+535.3086%)
L2 Accesses: 8 (-11.11111%)
RAM Accesses: 42 (+20.00000%)
Estimated Cycles: 321534 (+522.6091%)

¡Oh, estupendo! Iai nos dice que la diferencia entre los ciclos estimados de nuestros juegos FizzBuzz y FizzBuzzFibonacci es de +522.6091%. Tus números serán un poco diferentes a los míos. Sin embargo, la diferencia entre los dos juegos probablemente esté en el rango de 5x. ¡Eso me parece bien! Especialmente para agregar una función tan elegante como Fibonacci a nuestro juego. ¡A los niños les encantará!

Expande FizzBuzzFibonacci en Rust

¡Nuestro juego es todo un éxito! A los niños definitivamente les encanta jugar FizzBuzzFibonacci. Tanto así que llegó la noticia de los ejecutivos de que quieren una secuela. Pero este es el mundo moderno, ¡necesitamos ingresos recurrentes anuales (ARR) no compras únicas! La nueva visión para nuestro juego es que sea abierto, ¡no más limitaciones entre el 1 y el 100 (aunque sea inclusivo)! ¡No, vamos hacia nuevas fronteras!

Las reglas para Open World FizzBuzzFibonacci son las siguientes:

Escribe un programa que tome cualquier número entero positivo e imprima:

  • Para múltiplos de tres, imprime Fizz
  • Para múltiplos de cinco, imprime Buzz
  • Para múltiplos de tres y cinco, imprime FizzBuzz
  • Para números que son parte de la secuencia Fibonacci, sólo imprime Fibonacci
  • Para todos los demás, imprime el número

Para que nuestro juego funcione con cualquier número, necesitaremos aceptar un argumento de línea de comandos. Actualiza la función main para que se vea así:

fn main() {
let args: Vec<String> = std::env::args().collect();
let i = args
.get(1)
.map(|s| s.parse::<u32>())
.unwrap_or(Ok(15))
.unwrap_or(15);
play_game(i, true);
}
  • Recolecta todos los argumentos (args) pasados a nuestro juego desde la línea de comandos.
  • Obtén el primer argumento pasando a nuestro juego y analízalo como un entero sin signo i.
  • Si el análisis falla o no se pasa ningún argumento, por defecto, nuestro juego tomará el 15 como entrada.
  • Finalmente, juega nuestro juego con el nuevo entero sin signo i.

¡Ahora podemos jugar nuestro juego con cualquier número! Usa cargo run seguido de -- para pasar argumentos a nuestro juego:

$ cargo run -- 9
Compiling playground v0.0.1 (/home/bencher)
Finished dev [unoptimized + debuginfo] target(s) in 0.44s
Running `target/debug/game 9`
Fizz
$ cargo run -- 10
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/game 10`
Buzz
$ cargo run -- 13
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/game 13`
Fibonacci

Y si omitimos o proporcionamos un número inválido:

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/game`
FizzBuzz
$ cargo run -- bad
Finished dev [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/game bad`
FizzBuzz

Vaya, ¡eso fue una prueba exhaustiva! CI pasa. Nuestros jefes están emocionados. ¡Vamos a lanzarlo! 🚀

El Fin


SpongeBob SquarePants Tres Semanas Después
Meme de Esto está Bien

🐰 … ¿el fin de tu carrera tal vez?


¡Solo bromeaba! ¡Todo está en llamas! 🔥

Bueno, al principio todo parecía ir bien. Y luego a las 02:07 AM del sábado, mi buscapersonas sonó:

📟 ¡Tu juego está en llamas! 🔥

Después de salir de la cama a la carrera, traté de averiguar qué estaba pasando. Intenté buscar en los registros, pero eso fue difícil porque todo seguía fallando. Finalmente, encontré el problema. ¡Los niños! Les encantaba tanto nuestro juego, que lo estaban jugando hasta llegar al millón! En un destello de genialidad, agregué dos nuevos benchmarks:

fn bench_play_game_100() {
iai::black_box(play_game(100, false));
}
fn bench_play_game_1_000_000() {
iai::black_box(play_game(1_000_000, false));
}
  • Un micro-benchmark bench_play_game_100 para jugar el juego con el número cien (100)
  • Un micro-benchmark bench_play_game_1_000_000 para jugar el juego con el número un millón (1_000_000)

Cuando lo ejecuté, obtuve esto:

$ cargo bench
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 1.92s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 304598 (No change)
L1 Accesses: 320025 (+0.000312%)
L2 Accesses: 7 (-12.50000%)
RAM Accesses: 42 (No change)
Estimated Cycles: 321530 (-0.001244%)
bench_play_game_100
Instructions: 6194
L1 Accesses: 6290
L2 Accesses: 2
RAM Accesses: 11
Estimated Cycles: 6685

Espéralo… espéralo…

bench_play_game_1_000_000
Instructions: 155108715
L1 Accesses: 155108811
L2 Accesses: 2
RAM Accesses: 11
Estimated Cycles: 155109206

¡¿Qué?! 6,685 ciclos estimados x 1,000 deberían ser 6,685,000 ciclos estimados no 155,109,206 ciclos estimados 🤯 A pesar de que tengo mi código de secuencia Fibonacci funcionalmente correcto, debo tener un bug de rendimiento en algún lugar.

Corrección de FizzBuzzFibonacci en Rust

Volviendo a mirar la función is_fibonacci_number:

fn is_fibonacci_number(n: u32) -> bool {
for i in 0..=n {
let (mut previous, mut current) = (0, 1);
while current < i {
let next = previous + current;
previous = current;
current = next;
}
if current == n {
return true;
}
}
false
}

Ahora que estoy pensando en el rendimiento, me doy cuenta de que tengo un ciclo extra innecesario. Podemos deshacernos por completo del bucle for i in 0..=n {} y simplemente comparar el valor current con el número dado (n) 🤦

fn is_fibonacci_number(n: u32) -> bool {
let (mut previous, mut current) = (0, 1);
while current < n {
let next = previous + current;
previous = current;
current = next;
}
current == n
}
  • Actualiza tu función de is_fibonacci_number.
  • Inicializa nuestra secuencia de Fibonacci comenzando con el 0 y 1 como los números previous y current respectivamente.
  • Itera mientras que el número current sea menor al número dado n.
  • Suma el número previous y current para obtener el número next.
  • Actualiza el número previous al número current.
  • Actualiza el número current al número next.
  • Una vez que current es mayor o igual al número dado n, saldremos del bucle.
  • Comprobar si el número current es igual al número dado n y devolver ese resultado.

Ahora volvamos a ejecutar esos benchmarks y veamos cómo lo hicimos:

$ cargo bench
Compiling game v0.1.0 (/home/bencher)
Finished bench [optimized] target(s) in 4.22s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 38313 (-87.42178%)
L1 Accesses: 53739 (-83.20787%)
L2 Accesses: 7 (No change)
RAM Accesses: 43 (+2.380952%)
Estimated Cycles: 55279 (-82.80751%)
bench_play_game_100
Instructions: 295 (-95.23733%)
L1 Accesses: 389 (-93.81558%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (+18.18182%)
Estimated Cycles: 854 (-87.22513%)
bench_play_game_1_000_000
Instructions: 391 (-99.99975%)
L1 Accesses: 485 (-99.99969%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (+18.18182%)
Estimated Cycles: 950 (-99.99939%)

¡Oh, vaya! Nuestro benchmark bench_play_game ha vuelto a estar alrededor de donde estaba para el original FizzBuzz. Desearía poder recordar exactamente cuál era ese resultado. Pero han pasado tres semanas. Mi historial de terminal no llega tan lejos. Y Iai sólo compara contra el resultado más reciente. ¡Pero creo que está cerca!

El benchmark bench_play_game_100 ha descendido casi 10x, -87.22513%. ¡Y el benchmark bench_play_game_1_000_000 ha bajado más de 10,000x! De 155,109,206 ciclos estimados a 950 ciclos estimados! ¡Eso es -99.99939%!

🐰 Hey, al menos atrapamos este bug de rendimiento antes de que llegara a producción… oh, cierto. Olvida eso…

Detectar Retrocesos de Rendimiento en CI

Los ejecutivos no estaban contentos con la avalancha de críticas negativas que recibió nuestro juego debido a mi pequeño error de rendimiento. Me dijeron que no dejara que volviera a ocurrir, y cuando les pregunté cómo, simplemente me dijeron que no volviera a hacerlo. ¡¿Cómo se supone que debería manejar eso‽

Afortunadamente, he encontrado esta increíble herramienta de código abierto llamada Bencher. Hay un nivel gratuito súper generoso, así que puedo usar Bencher Cloud para mis proyectos personales. Y en el trabajo, donde todo debe estar en nuestra nube privada, he comenzado a usar Bencher Self-Hosted.

Bencher tiene adaptadores incorporados, por lo que es fácil de integrar en CI. Después de seguir la guía de inicio rápido, ya puedo ejecutar mis referencias y seguir su progreso con Bencher.

$ bencher run --project game "cargo bench"
Finished bench [optimized] target(s) in 0.18s
Running unittests src/lib.rs (target/release/deps/game-9b1b504669ca4b29)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/release/deps/game-8d61ca5a97299729)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running benches/play_game.rs (target/release/deps/play_game-6896309faf45cd96)
bench_play_game
Instructions: 38331 (+0.046981%)
L1 Accesses: 53765 (+0.048382%)
L2 Accesses: 6 (-14.28571%)
RAM Accesses: 45 (+4.651163%)
Estimated Cycles: 55370 (+0.164619%)
bench_play_game_100
Instructions: 313 (+6.101695%)
L1 Accesses: 416 (+6.940874%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (No change)
Estimated Cycles: 881 (+3.161593%)
bench_play_game_1_000_000
Instructions: 409 (+4.603581%)
L1 Accesses: 512 (+5.567010%)
L2 Accesses: 2 (No change)
RAM Accesses: 13 (No change)
Estimated Cycles: 977 (+2.842105%)
Finished bench [optimized] target(s) in 0.07s
Running unittests src/lib.rs (target/release/deps/game-13f4bad779fbfde4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Bencher New Report:
...
View results:
- bench_play_game (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=077449e5-5b45-4c00-bdfb-3a277413180d&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- bench_play_game_100 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=96508869-4fa2-44ac-8e60-b635b83a17b7&start_time=1697224006000&end_time=1699816009000&upper_boundary=true
- bench_play_game_1_000_000 (Latency): https://bencher.dev/console/projects/game/perf?measures=52507e04-ffd9-4021-b141-7d4b9f1e9194&branches=3a27b3ce-225c-4076-af7c-75adbc34ef9a&testbeds=bc05ed88-74c1-430d-b96a-5394fdd18bb0&benchmarks=ff014217-4570-42ea-8813-6ed0284500a4&start_time=1697224006000&end_time=1699816009000&upper_boundary=true

Usando este práctico dispositivo de viaje en el tiempo que un amable conejo me dio, pude retroceder en el tiempo y reproducir lo que habría sucedido si hubiéramos estado utilizando Bencher todo el tiempo. Puedes ver dónde publicamos por primera vez la implementación defectuosa de FizzBuzzFibonacci. Inmediatamente recibí fallas en CI como un comentario en mi solicitud de extracción. Ese mismo día, arreglé el error de rendimiento, eliminando ese bucle extra innecesario. No hubo incendios. Solo usuarios contentos.

Bencher: Benchmarking continuo

🐰 Bencher

Bencher es un conjunto de herramientas de benchmarking continuo. ¿Alguna vez has tenido un impacto de regresión de rendimiento en tus usuarios? Bencher podría haber evitado que eso sucediera. Bencher te permite detectar y prevenir las regresiones de rendimiento antes de que lleguen a producción.

  • Ejecutar: Ejecute sus benchmarks localmente o en CI usando sus herramientas de benchmarking favoritas. La CLI bencher simplemente envuelve su arnés de benchmarks existente y almacena sus resultados.
  • Seguir: Sigue los resultados de tus benchmarks con el tiempo. Monitoriza, realiza consultas y representa gráficamente los resultados utilizando la consola web de Bencher basándose en la rama de origen, el banco de pruebas y la medida.
  • Capturar: Captura las regresiones de rendimiento en CI. Bencher utiliza analíticas de vanguardia y personalizables para detectar regresiones de rendimiento antes de que lleguen a producción.

Por las mismas razones que las pruebas unitarias se ejecutan en CI para prevenir regresiones funcionales, los benchmarks deberían ejecutarse en CI con Bencher para prevenir regresiones de rendimiento. ¡Los errores de rendimiento son errores!

Empiece a capturar regresiones de rendimiento en CI — prueba Bencher Cloud gratis.

🤖 Este documento fue generado automáticamente por OpenAI GPT-4. Puede que no sea exacto y contenga errores. Si encuentra algún error, abra un problema en GitHub.