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


驴Qu茅 es el Benchmarking?

El benchmarking 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 olvidado en el desarrollo de software es crucial para crear y mantener un c贸digo r谩pido y de alto rendimiento. El benchmarking proporciona las m茅tricas necesarias para que los desarrolladores comprendan qu茅 tan bien se desempe帽a su c贸digo bajo diversas cargas de trabajo y condiciones. Por las mismas razones que escribes pruebas unitarias y de integraci贸n para prevenir regresiones de caracter铆sticas, debes escribir benchmarks para prevenir regresiones de rendimiento. 隆Los errores de rendimiento son errores!

驴Qu茅 es Rust?

Rust es un lenguaje de programaci贸n de c贸digo abierto que enfatiza la velocidad, la fiabilidad y la productividad. Logra garantizar la seguridad de la memoria sin la necesidad de un recolector de basura.

Deber铆as considerar usar Rust si est谩s escribiendo un:

  • Programa de bajo nivel donde el rendimiento es importante
  • Biblioteca compartida que ser谩 utilizada por varios idiomas diferentes
  • Interfaz de L铆nea de Comandos (CLI) compleja
  • Proyecto de software de larga duraci贸n con muchos colaboradores

Rust pone un fuerte 茅nfasis en la productividad del desarrollador. Cargo es el gestor de paquetes oficial, y se encarga de muchas tareas como:

  • Gesti贸n de dependencias de proyectos
  • Compilaci贸n de binarios, pruebas y puntos de referencia
  • Linting
  • Formateo

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.

Benchmarking en Rust

Las tres opciones populares para el benchmarking en Rust son: libtest bench, Criterion, y Iai.

libtest es el marco de pruebas unitarias y benchmarking incorporado en Rust. Aunque es parte de la librer铆a est谩ndar de Rust, libtest bench a煤n se considera inestable, por lo que solo est谩 disponible en las versiones nightly del compilador. Para trabajar en el compilador estable de Rust, se necesita usar un arn茅s de benchmarking separado. Sin embargo, ninguno de los dos est谩 en desarrollo activo.

El arn茅s de benchmarking m谩s activamente mantenida dentro del ecosistema de Rust es Criterion. Funciona tanto con las versiones de compilador estables como nightly de Rust, y se ha convertido en el est谩ndar dentro de la comunidad de Rust. Criterion tambi茅n es mucho m谩s rico en caracter铆sticas en comparaci贸n con libtest bench.

Una alternativa experimental a Criterion es Iai, del mismo creador que Criterion. Sin embargo, utiliza recuentos de instrucciones en lugar de tiempo de reloj de pared: instrucciones de CPU, accesos a L1, accesos a L2 y accesos a RAM. Esto permite el benchmarking de disparo 煤nico, ya que estas m茅tricas deber铆an permanecer 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 鈥渃aja 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.