Cómo construir un arnés de evaluación comparativa personalizado en Rust
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!
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.
Con todo eso en mente, si estás buscando medir el tiempo de reloj de tu código, entonces probablemente deberías usar Criterion. Si estás buscando medir el rendimiento de tu código en CI con runners compartidos, entonces puede valer la pena echarle un vistazo a Iai. Sin embargo, ten en cuenta que Iai no se ha actualizado en más de 3 años. Así que podrías considerar usar Iai-Callgrind en su lugar.
Pero, ¿qué pasa si no quieres medir el tiempo de reloj de pared o el conteo de instrucciones? ¿Qué pasa si quieres seguir un benchmark completamente diferente‽ Afortunadamente, Rust facilita increíblemente la creación de un arnés de pruebas personalizado.
Cómo Funciona cargo bench
Antes de construir un arnés de evaluación personalizado,
necesitamos entender cómo funcionan los benchmarks en Rust.
Para la mayoría de los desarrolladores de Rust, esto significa ejecutar el comando cargo bench
.
El comando cargo bench
compila y ejecuta tus benchmarks.
Por defecto, cargo bench
intentará usar el arnés de bench libtest incorporado (pero inestable).
libtest bench luego recorrerá tu código y ejecutará todas las funciones anotadas con el atributo #[bench]
.
Para usar un arnés de evaluación personalizado, necesitamos decirle a cargo bench
que no use libtest bench.
Usar un Harness de Benchmarking Personalizado con cargo bench
Para que cargo bench
no use libtest bench
,
necesitamos añadir lo siguiente a nuestro archivo Cargo.toml
:
Desafortunadamente, no podemos usar el atributo #[bench]
con nuestro harness de benchmarking personalizado.
Quizás algún día pronto, pero no hoy.
En su lugar, tenemos que crear un directorio separado benches
para guardar nuestros benchmarks.
El directorio benches
es para benchmarks
lo que el directorio tests
es para pruebas de integración.
Cada archivo dentro del directorio benches
se trata como un crate separado.
El crate que está siendo evaluado debe ser por lo tanto un crate de biblioteca.
Es decir, debe tener un archivo lib.rs
.
Por ejemplo, si tuviéramos un crate de biblioteca básico llamado game
entonces podríamos añadir un archivo de benchmark personalizado llamado play_game
al directorio benches
.
Nuestra estructura de directorios se vería así:
A continuación, necesitamos informar a cargo bench
sobre nuestro crate de benchmark personalizado play_game
.
Así que actualizamos nuestro archivo Cargo.toml
:
Escribir código para medir el rendimiento
Antes de que podamos escribir una prueba de rendimiento, necesitamos tener algún código de biblioteca para evaluar. Para nuestro ejemplo, vamos a jugar al 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 ambos tres y cinco, imprime
FizzBuzz
- Para números que son parte de la secuencia de Fibonacci, solo imprime
Fibonacci
- Para todos los demás, imprime el número
Así es como se ve nuestra implementación en src/lib.rs
:
Crear un Arnés de Benchmarking Personalizado
Vamos a crear un arnés de benchmarking personalizado dentro de benches/play_game.rs
.
Este arnés de benchmarking personalizado va a medir las asignaciones en el heap
usando el crate dhat-rs
.
dhat-rs
es una herramienta fantástica para rastrear las asignaciones en el heap en programas de Rust
creada por el experto en rendimiento de Rust Nicholas Nethercote.
Para ayudarnos a gestionar nuestras funciones de benchmark,
usaremos el crate inventory
del asombrosamente prolífico David Tolnay.
Agreguemos dhat-rs
e inventory
a nuestro archivo Cargo.toml
como dev-dependencies
:
Crear un Asignador Personalizado
Dado que nuestro arnés de evaluación personalizado medirá las asignaciones en el montón,
necesitaremos usar un asignador de montón personalizado.
Rust permite configurar un asignador de montón global personalizado
usando el atributo #[global_allocator]
.
Añade lo siguiente al principio de benches/play_game.rs
:
Esto le dice a Rust que use dhat::Alloc
como nuestro asignador de montón global.
🐰 Solo puedes establecer un asignador de montón global a la vez. Si deseas cambiar entre varios asignadores globales, tienen que ser administrados a través de la compilación condicional con [características de Rust].
Crear un Colector de Benchmarks Personalizado
Para crear un arnés de evaluación comparativa personalizado,
necesitamos una forma de identificar y almacenar nuestras funciones de benchmark.
Usaremos una estructura, acertadamente llamada CustomBenchmark
para encapsular cada función de benchmark.
Un CustomBenchmark
tiene un nombre y una función de benchmark que devuelve dhat::HeapStats
como resultado.
Luego usaremos el crate inventory
para crear una colección de todos nuestros CustomBenchmark
s:
Crear una Función de Benchmark
Ahora, podemos crear una función de benchmark que juegue el juego FizzBuzzFibonacci:
Línea por línea:
- Crear una función de benchmark que coincida con la firma utilizada en
CustomBenchmark
. - Crear un
dhat::Profiler
en modo de prueba, para recopilar resultados de nuestrodhat::Alloc
asignador global personalizado. - Ejecutar nuestra función
play_game
dentro de una “caja negra” para que el compilador no optimice nuestro código. - Iterar de
1
a100
inclusivamente. - Para cada número, llamar a
play_game
, conprint
establecido enfalse
. - Devolver nuestras estadísticas de asignación de heap como
dhat::HeapStats
.
🐰 Establecemos
false
para la funciónplay_game
. Esto evita queplay_game
imprima en la salida estándar. Parametrizar las funciones de tu librería de esta manera puede hacerlas más adecuadas para el benchmarking. Sin embargo, esto significa que puede que no estemos evaluando la librería de la misma manera que se usa en producción.En este caso, debemos preguntarnos:
- ¿Nos importan los recursos que se necesitan para imprimir en la salida estándar?
- ¿Es la impresión en la salida estándar una posible fuente de ruido?
Para nuestro ejemplo, hemos decidido:
- No, no nos importa imprimir en la salida estándar.
- Sí, es una fuente de ruido muy probable.
Por lo tanto, hemos omitido la impresión en la salida estándar como parte de este benchmark. El benchmarking es difícil y, a menudo, no hay una respuesta correcta a preguntas como estas. Depende.
Registrar la Función de Referencia
Con nuestra función de referencia escrita, necesitamos
crear un CustomBenchmark
y registrarlo en nuestra colección de referencias usando inventory
.
Si tuviéramos más de una referencia, repetiríamos este mismo proceso:
- Crear una función de referencia.
- Crear un
CustomBenchmark
para la función de referencia. - Registrar el
CustomBenchmark
en la coleccióninventory
.
Crear un Ejecutador de Benchmark Personalizado
Finalmente, necesitamos crear un ejecutor para nuestro marco de benchmarks personalizado. Un marco de benchmarks personalizado es realmente solo una aplicación binaria que ejecuta todos nuestros benchmarks y reporta sus resultados. El ejecutor de benchmarks es lo que orquesta todo eso.
Queremos que nuestros resultados se impriman en Formato de Métrica Bencher (BMF) JSON.
Para lograr esto, necesitamos añadir una última dependencia,
el serde_json
crate por… adivinaste, ¡David Tolnay!
A continuación, implementaremos un método para CustomBenchmark
para ejecutar su función de benchmark
y luego devolver los resultados en BMF JSON.
Los resultados en BMF JSON contienen seis Medidas para cada benchmark:
- Bloques Finales: Número final de bloques asignados cuando el benchmark terminó.
- Bytes Finales: Número final de bytes asignados cuando el benchmark terminó.
- Bloques Máximos: Máximo número de bloques asignados en un momento dado durante la ejecución del benchmark.
- Bytes Máximos: Máximo número de bytes asignados en un momento dado durante la ejecución del benchmark.
- Bloques Totales: Número total de bloques asignados durante la ejecución del benchmark.
- Bytes Totales: Número total de bytes asignados durante la ejecución del benchmark.
Finalmente, podemos crear una función main
para ejecutar todos los benchmarks en nuestra colección inventory
y imprimir los resultados en BMF JSON.
Ejecutar el Arnés de Benchmark Personalizado
Todo está ahora en su lugar. Finalmente podemos ejecutar nuestro arnés de benchmark personalizado.
La salida tanto a la salida estándar
como a un archivo llamado results.json
debería verse así:
Los números exactos que veas pueden ser un poco diferentes según la arquitectura de tu computadora. Pero lo importante es que al menos tengas algunos valores para las últimas cuatro métricas.
Rastrea Resultados de Benchmarks Personalizados
La mayoría de los resultados de benchmarks son efímeros. Desaparecen tan pronto como el límite de retroceso en tu terminal se alcanza. Algunos arneses de benchmarks te permiten almacenar en caché los resultados, pero eso requiere mucho trabajo para implementar. E incluso entonces, solo podríamos almacenar nuestros resultados localmente. ¡Por suerte para nosotros, nuestro arnés de benchmarking personalizado funcionará con Bencher! Bencher es un conjunto de herramientas de benchmarking continuo que nos permite rastrear los resultados de nuestros benchmarks a lo largo del tiempo y detectar regresiones de rendimiento antes de que lleguen a producción.
Una vez que estés configurado utilizando Bencher Cloud o Bencher Self-Hosted, puedes rastrear los resultados de nuestro arnés de benchmarking personalizado ejecutando:
También puedes leer más sobre cómo rastrear benchmarks personalizados con Bencher y el adaptador de benchmarks JSON.
Conclusión
Comenzamos este artículo examinando los tres arneses de evaluación comparativa más populares en el ecosistema de Rust: libtest bench, Criterion, y Iai. Aunque pueden cubrir la mayoría de los casos de uso, a veces es posible que necesites medir algo más que el tiempo de reloj de pared o el recuento de instrucciones. Esto nos llevó a crear un arnés de evaluación comparativa personalizado.
Nuestro arnés de evaluación comparativa personalizado mide las asignaciones en el montón utilizando dhat-rs
. Las funciones de evaluación se recopilaron utilizando inventory
. Al ejecutarse, nuestras evaluaciones producen resultados en formato JSON de Bencher Metric Format (BMF). Luego podríamos usar Bencher para rastrear nuestros resultados personalizados de evaluación comparativa a lo largo del tiempo y detectar regresiones de rendimiento en CI.
Todo el código fuente de esta guía está disponible en GitHub.
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.