Cómo hacer pruebas de rendimiento en Rust con el banco de pruebas libtest
Everett Pompeii
¿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
al100
(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:
- Crea una función
main
- Itera desde
1
hasta100
de manera inclusiva. - Para cada número, calcula el módulo (resto después de la división) tanto para
3
como para5
. - 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 para3
como para5
, imprimeFizzBuzz
. - Si el resto es
0
solo para3
, entonces imprimeFizz
. - Si el resto es
0
solo para5
, entonces imprimeBuzz
. - 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.
Deberías ver un directorio llamado src
con un archivo llamado main.rs
:
Reemplaza sus contenidos con la implementación de FizzBuzz de arriba. Luego ejecuta cargo run
.
La salida debería verse así:
🐰 ¡Boom! ¡Estás rompiendo la entrevista de codificación!
Se debería haber generado un nuevo archivo Cargo.lock
:
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 soportados por Bencher. Entonces, ¿por qué elegir banco de pruebas libtest?
Puede ser una buena idea si estás tratando de limitar las dependencias externas de tu proyecto
y tu proyecto ya está usando la cadena de herramientas nightly
.
Fuera de eso, sugeriría usar Criterion o Iai dependiendo de tu caso de uso.
Instalar Rust nightly
Dicho esto, vamos a usar el banco de pruebas libtest, así que vamos a poner nuestra cadena de herramientas de Rust en nightly
.
Crea un archivo rust-toolchain.toml
en la raíz de tu proyecto game
, junto a Cargo.toml
.
Tu estructura de directorios ahora debería lucir así:
Una vez que esté completo, vuelve a ejecutar cargo run
.
Debería llevar un minuto para que la nueva cadena de herramientas nightly se instale
antes de volver a ejecutar y darte la misma salida que antes.
Refactorizar FizzBuzz
Para probar nuestra aplicación FizzBuzz, necesitamos desacoplar nuestra lógica de la función main
del programa.
Las superestructuras de rendimiento no pueden medir el rendimiento de la función main
.
Actualiza tu código para que se vea así:
Ahora hemos separado nuestro código en tres funciones diferentes:
main
: El punto de entrada principal a nuestro programa que itera a través de los números del1
al100
, inclusive, y llama aplay_game
para cada número.play_game
: Toma un número entero sin signon
, llama afizz_buzz
con ese número, e imprime el resultado.fizz_buzz
: Toma un número entero sin signon
y realiza la lógica deFizz
,Buzz
,FizzBuzz
o número, devolviendo el resultado como una cadena.
Haciendo benchmark de FizzBuzz
Para utilizar el inestable crate libtest necesitamos habilitar la feature test
para nuestro código e importar el crate test
. Añade lo siguiente en la parte superior de main.rs
:
¡Ahora estamos listos para añadir nuestro primer benchmark!
Añade lo siguiente en la parte inferior de main.rs
:
- Crea un módulo llamado
benchmarks
y establece la configuración del compilador a modotest
. - Importa el runner de benchmark
Bencher
. (🐰 ¡Hey, qué nombre tan cool!) - Importa nuestra función
play_game
. - Crea un benchmark llamado
bench_play_game
que recibe una referencia mutable aBencher
. - Establece el atributo
#[bench]
para indicar quebench_play_game
es un benchmark. - Usa la instancia
Bencher
(b
) para ejecutar nuestro macro-benchmark varias veces. - Ejecuta nuestro macro-benchmark dentro de una “caja negra” para que el compilador no optimice nuestro código.
- Itera desde
1
hasta100
inclusive. - Para cada número, llama a
play_game
.
Ahora estamos listos para hacer un benchmark de nuestro código, ejecuta cargo bench
:
🐰 ¡Encendamos el ritmo! ¡Tenemos nuestras primeras métricas de benchmark!
Finalmente, podemos descansar nuestras cansadas cabezas de desarrolladores… Sólo 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
al100
(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:
- 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 dadon
inclusive. - Inicializa nuestra secuencia Fibonacci comenzando con
0
y1
como los númerosanterior
yactual
respectivamente. - Itera mientras el número
actual
sea menor que la iteración actuali
. - Suma el número
anterior
y el númeroactual
para obtener el númerosiguiente
. - Actualiza el número
anterior
al númeroactual
. - Actualiza el número
actual
al númerosiguiente
. - Una vez que
actual
sea mayor o igual al número dadon
, saldremos del bucle. - Verifica si el número
actual
es igual al número dadon
y si es así, devuelvetrue
. - De lo contrario, devuelve
false
.
Ahora necesitaremos actualizar nuestra función fizz_buzz
:
- Renombra la función
fizz_buzz
afizz_buzz_fibonacci
para que sea más descriptiva. - Llama a nuestra función auxiliar
is_fibonacci_number
. - Si el resultado de
is_fibonacci_number
estrue
, entonces devuelveFibonacci
. - Si el resultado de
is_fibonacci_number
esfalse
, entonces realiza la misma lógica deFizz
,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
:
Tanto nuestras funciones main
como bench_play_game
pueden permanecer exactamente iguales.
Haciendo Benchmark de FizzBuzzFibonacci
Ahora podemos volver a ejecutar nuestro benchmark:
Desplazándonos hacia atrás a través de nuestro historial de terminal,
podemos hacer una comparación visual entre el rendimiento de nuestros juegos FizzBuzz y FizzBuzzFibonacci: 4,879 ns
vs 22,167 ns
.
Tus números serán un poco diferentes a los míos.
Sin embargo, la diferencia entre los dos juegos probablemente sea de alrededor de 5 veces.
¡Eso me parece bien! Especialmente por agregar una función tan sofisticada 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í:
- 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:
Y si omitimos o proporcionamos un número inválido:
Vaya, ¡eso fue una prueba exhaustiva! CI pasa. Nuestros jefes están emocionados. ¡Vamos a lanzarlo! 🚀
El Fin
🐰 … ¿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:
- 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:
Espéralo… espéralo…
¡Qué! 439 ns
x 1,000
debería ser 439,000 ns
no 9,586,977 ns
🤯
Aunque obtuve mi función de código de secuencia de Fibonacci funcionalmente correcta, debo tener un error de rendimiento en algún lugar.
Corrección de FizzBuzzFibonacci en Rust
Volviendo a mirar la función is_fibonacci_number
:
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
) 🤦
- Actualiza tu función de
is_fibonacci_number
. - Inicializa nuestra secuencia de Fibonacci comenzando con el
0
y1
como los númerosprevious
ycurrent
respectivamente. - Itera mientras que el número
current
sea menor al número dadon
. - Suma el número
previous
ycurrent
para obtener el númeronext
. - Actualiza el número
previous
al númerocurrent
. - Actualiza el número
current
al númeronext
. - Una vez que
current
es mayor o igual al número dadon
, saldremos del bucle. - Comprobar si el número
current
es igual al número dadon
y devolver ese resultado.
Ahora vamos a volver a ejecutar esas pruebas y ver cómo nos fue:
¡Oh, vaya! Nuestro benchmark bench_play_game
ha vuelto a estar cerca de donde estaba para el original FizzBuzz.
Ojalá pudiera recordar exactamente cuál era esa puntuación. Han pasado tres semanas.
Mi historial de terminal no llega tan lejos.
¡Pero creo que está cerca!
El benchmark bench_play_game_100
ha bajado casi 10 veces, de 439 ns
a 46 ns
.
¡Y el benchmark bench_play_game_1_000_000
ha bajado más de 10,000 veces! ¡De 9,586,977 ns
a 53 ns
!
🐰 Hey, al menos detectamos este error de rendimiento antes de que llegara a producción… oh, cierto. No importa…
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.
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 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.