Como fazer benchmark de código Rust com Gungraun
Everett Pompeii
O que é Benchmarking?
Benchmarking é a prática de testar o desempenho do seu código para ver quão rápido (latência) ou quanto (throughput) trabalho ele pode executar. Este passo frequentemente negligenciado no desenvolvimento de software é crucial para criar e manter um código rápido e performático. O benchmarking fornece as métricas necessárias para que os desenvolvedores compreendam o desempenho do seu código sob várias cargas de trabalho e condições. Pelas mesmas razões que você escreve testes unitários e de integração para evitar regressões de funcionalidades, você deve escrever benchmarks para evitar regressões de desempenho. Bugs de desempenho são bugs!
Escreva FizzBuzz em Rust
Para escrevermos testes de desempenho, precisamos de algum código-fonte para testar. Para começar, vamos escrever um programa muito simples, FizzBuzz.
As regras para o FizzBuzz são as seguintes:
Escreva um programa que imprima os inteiros de
1a100(inclusive):
- Para múltiplos de três, imprima
Fizz- Para múltiplos de cinco, imprima
Buzz- Para múltiplos de três e cinco, imprima
FizzBuzz- Para todos os outros, imprima o número
Existem muitas maneiras de escrever o FizzBuzz. Então vamos seguir com o meu favorito:
fn main() { for i in 1..=100 { match (i % 3, i % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), (_, _) => println!("{i}"), } }}- Crie uma função
main - Itere de
1a100inclusivamente. - Para cada número, calcule o módulo (resto depois da divisão) para ambos
3e5. - Faça correspondência de padrões nos dois restos.
Se o resto é
0, então o número é múltiplo do fator dado. - Se o resto é
0para ambos3e5então imprimaFizzBuzz. - Se o resto é
0apenas para3então imprimaFizz. - Se o resto é
0apenas para5então imprimaBuzz. - Caso contrário, apenas imprima o número.
Siga Passo a Passo
Para acompanhar este tutorial passo a passo, você precisa instalar Rust.
🐰 O código fonte para esta postagem está disponível no GitHub
Com Rust instalado, você pode então abrir uma janela de terminal e digitar: cargo init game
Em seguida, navegue para o diretório game recém-criado.
game├── Cargo.toml└── src └── main.rsVocê verá um diretório chamado src com um arquivo chamado main.rs:
fn main() { println!("Hello, world!");}Substitua o conteúdo dele pela implementação FizzBuzz acima. Depois, execute cargo run.
A saída deve ser parecida com:
$ cargo run Compiling playground v0.0.1 (/home/bencher) Finished dev [unoptimized + debuginfo] target(s) in 0.44s Running `target/debug/game`
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz...9798FizzBuzz🐰 Boom! Você está arrasando na entrevista de programação!
Um novo arquivo Cargo.lock deve ter sido gerado:
game├── Cargo.lock├── Cargo.toml└── src └── main.rsAntes de prosseguir, é importante discutir as diferenças entre micro-benchmarking e macro-benchmarking.
Micro-Benchmarking vs Macro-Benchmarking
Existem duas categorias importantes de benchmarks de software: micro-benchmarks e macro-benchmarks.
Os micro-benchmarks operam em um nível semelhante aos testes unitários.
Por exemplo, um benchmark para uma função que determina Fizz, Buzz, ou FizzBuzz para um número individual seria um micro-benchmark.
Os macro-benchmarks operam em um nível semelhante aos testes de integração.
Por exemplo, um benchmark para uma função que executa o jogo inteiro de FizzBuzz, de 1 a 100, seria um macro-benchmark.
Em geral, é melhor testar no menor nível de abstração possível. No caso dos benchmarks, isso os torna mais fáceis de manter, e ajuda a reduzir a quantidade de ruído nas medições. No entanto, assim como ter alguns testes de ponta a ponta pode ser muito útil para verificar se todo o sistema se junta conforme esperado, ter macro-benchmarks pode ser muito útil para garantir que os caminhos críticos através do seu software permaneçam com bom desempenho.
Benchmarking em Rust
As três opções populares para benchmarking em Rust são: libtest bench, Criterion, e Iai.
libtest é o framework embutido de testes unitários e benchmarking do Rust.
Embora faça parte da biblioteca padrão do Rust, o libtest bench ainda é considerado instável,
portanto, está disponível apenas em versões do compilador nightly.
Para funcionar no compilador Rust estável,
um harness de benchmarking separado
precisa ser usado.
Nenhum dos dois está sendo desenvolvido ativamente, no entanto.
O harness de benchmarking mais popular dentro do ecossistema Rust é o Criterion.
Ele funciona tanto em versões estáveis quanto em versões nightly do compilador Rust,
e se tornou o padrão de facto dentro da comunidade Rust.
O Criterion também possui muito mais recursos em comparação com o libtest bench.
Uma alternativa experimental ao Criterion é o Iai, do mesmo criador do Criterion. No entanto, ele usa contagem de instruções em vez de tempo real: instruções da CPU, acessos L1, acessos L2 e acessos à RAM. Isso permite benchmarking de tiro único, uma vez que essas métricas devem permanecer quase idênticas entre as execuções.
Todos os quatro são suportados pelo Bencher. Então por que escolher o Gungraun (o sucessor renomeado do Iai-Callgrind)? O Gungraun usa contagens de instruções em vez de tempo de relógio de parede. Isso o torna ideal para benchmarking contínuo, ou seja, benchmarking em CI. Eu sugeriria usar o Gungraun para benchmarking contínuo, especialmente se você estiver usando runners compartilhados. O Gungraun é ativamente mantido e tem documentação online abrangente, tornando-o uma escolha confiável para projetos de longo prazo. É importante entender que o Gungraun mede apenas um proxy do que você realmente se importa. Ir de 1.000 instruções para 2.000 instruções dobra a latência da sua aplicação? Talvez sim, talvez não. Por essa razão, pode ser útil também executar benchmarks baseados em tempo de relógio de parede em paralelo com benchmarks baseados em contagem de instruções.
Instalar Valgrind
O Gungraun usa uma ferramenta chamada Valgrind para coletar contagens de instruções. O Valgrind suporta Linux, Solaris, FreeBSD e macOS. No entanto, o suporte ao macOS é limitado a processadores x86_64, pois processadores arm64 (M1, M2, etc.) ainda não são suportados.
No Debian execute: sudo apt-get install valgrind
No macOS (apenas chip x86_64/Intel): brew install valgrind
Refatorar FizzBuzz
Para testar nossa aplicação FizzBuzz, desacoplamos nossa lógica da função main do nosso programa.
Ao contrário de outros arneses de benchmark, o Gungraun pode fazer benchmark do binário de benchmark
e da função main, mas isso é puramente macro-benchmarking. Queremos fazer ambos, macro e micro.
Para fazer isso, precisamos fazer algumas alterações.
Sob src, crie um novo arquivo chamado lib.rs:
game├── Cargo.lock├── Cargo.toml└── src └── lib.rs └── main.rsAdicione o seguinte código ao 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: Recebe um inteiro sem sinaln, chamafizz_buzzcom esse número, e seprintfortrueimprime o resultado.fizz_buzz: Recebe um inteiro sem sinalne executa a lógica real deFizz,Buzz,FizzBuzzou número, retornando o resultado como uma string.
Então o main.rs atualizado fica assim:
use game::play_game;
fn main() { for i in 1..=100 { play_game(i, true); }}game::play_game: Importaplay_gamedo crategameque acabamos de criar comlib.rs.main: O ponto de entrada principal do nosso programa que itera pelos números de1a100inclusive e chamaplay_gamepara cada número, comprintdefinido comotrue.
Benchmarking do FizzBuzz
Para fazer benchmark do nosso código, precisamos criar um diretório benches e
adicionar um arquivo para conter nossos benchmarks, play_game.rs.
Note que estamos desviando da forma recomendada
de estruturar benchmarks por simplicidade.
Para seu projeto, você deve seguir as recomendações:
game├── Cargo.lock├── Cargo.toml└── benches └── play_game.rs└── src └── lib.rs └── main.rsDentro de play_game.rs adicione o seguinte código:
use gungraun::prelude::*;use std::hint::black_box;use game::play_game;
#[library_benchmark]fn bench_play_game() { for i in 1..=100 { play_game(black_box(i), black_box(false)) }}
library_benchmark_group!( name = bench_play_game_group, benchmarks = [bench_play_game]);
main!(library_benchmark_groups = bench_play_game_group);- Importe o módulo
gungraun::preludeque traz os macros necessários. - Use
std::hint::black_boxpara evitar que o compilador otimize nosso benchmark. - Importe a função
play_gamedo nosso crategame. - Crie uma função de benchmark de biblioteca chamada
bench_play_gameusando o atributo#[library_benchmark]. - Itere de
1a100e chameplay_gamecomprintdefinido comofalse. - Crie um grupo de benchmarks de biblioteca chamado
bench_play_game_groupcontendo nosso benchmarkbench_play_game. - Use o macro
main!para executar o grupo de benchmarks.
Agora, precisamos configurar o crate game para executar nossos benchmarks.
Adicione o seguinte ao final do seu arquivo Cargo.toml:
[dev-dependencies]gungraun = "0.18.0"
[[bench]]name = "play_game"harness = false
[profile.bench]debug = truegungraun: Adicionegungrauncomo dependência de desenvolvimento, já que estamos usando apenas para testes de performance.bench: Registreplay_gamecomo benchmark e definaharnesscomofalse, pois usaremos o Gungraun como nosso arnês de benchmarking.debug = true: Habilite informações de depuração em builds de benchmark, o que é necessário para o Gungraun fornecer saída detalhada.
Agora estamos prontos para fazer benchmark do nosso código, execute cargo bench:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 17902|N/A (*********) L1 Hits: 24984|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 20|N/A (*********) Total read+write: 25005|N/A (*********) Estimated Cycles: 25689|N/A (*********)
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15258s🐰 Alface nabo a beterraba! Temos nossas primeiras métricas de benchmark!
Finalmente, podemos descansar nossas cabeças cansadas de desenvolvedores… Brincadeira, nossos usuários querem uma nova funcionalidade!
Escreva FizzBuzzFibonacci em Rust
Nossos Indicadores Chave de Desempenho (KPIs) estão em baixa, então nosso Gerente de Produto (PM) quer que adicionemos um novo recurso. Após muitas discussões e várias entrevistas com usuários, decidiu-se que o bom e velho FizzBuzz não é suficiente. As crianças de hoje querem um novo jogo, FizzBuzzFibonacci.
As regras para FizzBuzzFibonacci são as seguintes:
Escreva um programa que imprime os números inteiros de
1a100(inclusive):
- Para múltiplos de três, imprima
Fizz- Para múltiplos de cinco, imprima
Buzz- Para múltiplos de ambos três e cinco, imprima
FizzBuzz- Para números que fazem parte da sequência de Fibonacci, apenas imprima
Fibonacci- Para todos os outros, imprima o número
A Sequência de Fibonacci é uma série na qual cada número é a soma dos dois números precedentes.
Por exemplo, começando com 0 e 1 o próximo número na sequência de Fibonacci seria 1.
Seguido por: 2, 3, 5, 8 e assim por diante.
Números que fazem parte da Sequência de Fibonacci são conhecidos como números de Fibonacci. Então, teremos que escrever uma função que detecte números de Fibonacci.
Existem muitas maneiras de escrever a sequência de Fibonacci e, da mesma forma, muitas maneiras de detectar um número de Fibonacci. Então, vamos com a minha 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}- Crie uma função chamada
is_fibonacci_numberque recebe um número inteiro sem sinal e retorna um booleano. - Itere para todos os números de
0ao nosso número específiconinclusive. - Inicialize nossa sequência Fibonacci começando com
0e1como os númerosanterioreatualrespectivamente. - Itere enquanto o número
atualfor menor que a iteração atuali. - Adicione o número
atualeanteriorpara obter o númeropróximo. - Atualize o número
anteriorpara o númeroatual. - Atualize o número
atualpara o númeropróximo. - Uma vez que
atualfor maior ou igual ao número especificon, nós sairemos do loop. - Verifique se o número
atualé igual ao número especificadone, se for, retornetrue. - Caso contrário, retorne
false.
Agora precisaremos atualizar nossa função 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(), } }}- Renomeie a função
fizz_buzzparafizz_buzz_fibonaccipara torná-la mais descritiva. - Chame nossa função auxiliar
is_fibonacci_number. - Se o resultado de
is_fibonacci_numberfortrueretorneFibonacci. - Se o resultado de
is_fibonacci_numberforfalse, execute a mesma lógicaFizz,Buzz,FizzBuzz, ou número retornando o resultado.
Como renomeamos fizz_buzz para fizz_buzz_fibonacci, também precisamos atualizar nossa função play_game:
pub fn play_game(n: u32, print: bool) { let result = fizz_buzz_fibonacci(n); if print { println!("{result}"); }}Ambas as funções main e bench_play_game podem permanecer exatamente as mesmas.
Benchmarking do FizzBuzzFibonacci
Agora podemos executar novamente nosso benchmark:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 331835|17902 (+1753.62%) [+18.5362x] L1 Hits: 338828|24984 (+1256.18%) [+13.5618x] LL Hits: 2|1 (+100.000%) [+2.00000x] RAM Hits: 22|20 (+10.0000%) [+1.10000x] Total read+write: 338852|25005 (+1255.14%) [+13.5514x] Estimated Cycles: 339608|25689 (+1222.00%) [+13.2200x]
Gungraun result: Ok. 1 without regressions; 0 regressed; 0 filtered; 1 benchmarks finished in 0.15254sOh, legal! O Gungraun nos mostra a diferença entre os ciclos estimados dos nossos jogos FizzBuzz e FizzBuzzFibonacci.
Seus números serão um pouco diferentes dos meus.
No entanto, a diferença entre os dois jogos provavelmente está na faixa de 10-15x.
Isso me parece bom! Especialmente por adicionar uma funcionalidade tão sofisticada como Fibonacci ao nosso jogo.
As crianças vão adorar!
Expandindo FizzBuzzFibonacci em Rust
Nosso jogo é um sucesso! As crianças realmente adoram jogar FizzBuzzFibonacci.
Tanto que a direção quer uma sequência.
Mas vivemos em um mundo moderno, precisamos de Receita Anual Recorrente (ARR) e não de compras únicas!
A nova visão para o nosso jogo é que ele seja aberto, sem mais viver entre os limites de 1 e 100 (mesmo que inclusivo).
Não, estamos partindo para novas fronteiras!
As regras para o Open World FizzBuzzFibonacci são as seguintes:
Escreva um programa que aceite qualquer número inteiro positivo e imprima:
- Para múltiplos de três, imprima
Fizz- Para múltiplos de cinco, imprima
Buzz- Para múltiplos de ambos três e cinco, imprima
FizzBuzz- Para números que fazem parte da sequência de Fibonacci, apenas imprima
Fibonacci- Para todos os outros, imprima o número
Para que nosso jogo funcione para qualquer número, precisaremos aceitar um argumento de linha de comando.
Atualize a função main para ficar assim:
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);}- Colete todos os argumentos (
args) passados para o nosso jogo a partir da linha de comando. - Pegue o primeiro argumento passado para o nosso jogo e analise-o como um inteiro não assinado
i. - Se a análise falhar ou nenhum argumento for passado, use por padrão o nosso jogo com
15como entrada. - Finalmente, jogue nosso jogo com o novo inteiro não assinado
ianalisado.
Agora podemos jogar nosso jogo com qualquer número!
Use cargo run seguido de -- para passar argumentos para o nosso jogo:
$ 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`FibonacciE se omitirmos ou fornecermos um 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`FizzBuzzNossa, que teste completo! O CI passou. Nossos chefes estão entusiasmados. Vamos lançá-lo! 🚀
O Fim


🐰 … o fim da sua carreira talvez?
Brincadeira! Tudo está pegando fogo! 🔥
Bem, a princípio, tudo parecia estar indo bem. Então, às 02:07 da madrugada de sábado, meu pager disparou:
📟 Seu jogo está pegando fogo! 🔥
Após sair da cama às pressas, tentei descobrir o que estava acontecendo. Eu tentei pesquisar nos logs, mas era difícil porque tudo continuava travando. Finalmente, encontrei o problema. As crianças! Elas adoravam tanto nosso jogo que jogavam até chegar a um milhão! Num lampejo de brilhantismo, adicionei dois novos benchmarks:
É aqui que os benchmarks parametrizados do Gungraun brilham! Em vez de escrever funções de benchmark separadas para cada entrada, podemos usar o atributo #[benches::...]:
#[library_benchmark]#[benches::play(100, 1_000_000)]fn bench_play_game(n: u32) { play_game(black_box(n), black_box(false));}
library_benchmark_group!( name = bench_play_game_group, benchmarks = [bench_play_game_100, bench_play_game]);- Adicione o atributo
#[benches::play(100, 1_000_000)]para criar uma variante de benchmark com entrada100e outra com entrada1_000_000. - A função de benchmark recebe um parâmetro
n: u32que recebe cada valor. - Adicione a função
bench_play_gameaolibrary_benchmark_group!
Legal! Uma função de benchmark, múltiplos casos de teste!
Quando executei, obtive isto:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 331835|331835 (No change) L1 Hits: 338831|338828 (+0.00089%) [+1.00001x] LL Hits: 1|2 (-50.0000%) [-2.00000x] RAM Hits: 20|22 (-9.09091%) [-1.10000x] Total read+write: 338852|338852 (No change) Estimated Cycles: 339536|339608 (-0.02120%) [-1.00021x]play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 7072|N/A (*********) L1 Hits: 7128|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 7138|N/A (*********) Estimated Cycles: 7448|N/A (*********)play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 183930316|N/A (*********) L1 Hits: 183930372|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 183930382|N/A (*********) Estimated Cycles: 183930692|N/A (*********)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 1.45441sBenchmark concluído em 1,45 segundos. Isso foi rápido! Em vez de executar benchmarks
várias vezes como benchmarks de tempo de relógio de parede fazem,
cada benchmark do Gungraun executa apenas uma vez. Mas espere, por que há mudanças no primeiro benchmark bench_play_game_100,
embora não tenhamos mudado nada neste benchmark?
Correto, mas mudamos algo diferente no arquivo de benchmark e como o Gungraun e o Valgrind
são instrumentos sensíveis, mesmo mudanças muito pequenas são registradas.
No entanto, essas pequenas mudanças, especialmente nas métricas de cache, são insignificantes. Com o tempo, você
desenvolverá um senso para mudanças críticas nas métricas. Vamos olhar mais de perto nossa saída.
play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 183930316|N/A (*********) L1 Hits: 183930372|N/A (*********) LL Hits: 1|N/A (*********) RAM Hits: 9|N/A (*********) Total read+write: 183930382|N/A (*********) Estimated Cycles: 183930692|N/A (*********)O quê! 7.448 ciclos estimados x 1.000 deveria ser 7.448.000 ciclos estimados não 183.930.692 ciclos estimados 🤯
Embora eu tenha conseguido meu código de sequência Fibonacci funcionalmente correto, devo ter um bug de performance em algum lugar.
Corrigindo FizzBuzzFibonacci em Rust
Vamos dar outra olhada naquela função 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}Agora que estou pensando em desempenho, percebo que tenho um loop extra desnecessário.
Podemos nos livrar completamente do loop for i in 0..=n {} e
apenas comparar o valor atual com o 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}- Atualize sua função
is_fibonacci_number. - Inicialize nossa sequência de Fibonacci começando com
0e1como os númerosanterioreatual, respectivamente. - Itere enquanto o número
atualfor menor que o número dadon. - Adicione o número
anterioreatualpara obter o númeropróximo. - Atualize o número
anteriorpara o númeroatual. - Atualize o número
atualpara o númeropróximo. - Uma vez que
atualseja maior ou igual ao número dadon, sairemos do loop. - Verifique se o número
atualé igual ao número dadone retorne esse resultado.
Agora vamos executar novamente esses benchmarks e ver como nos saímos:
$ cargo bench Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 23679|331835 (-92.8642%) [-14.0139x] L1 Hits: 30675|338831 (-90.9468%) [-11.0458x] LL Hits: 2|1 (+100.000%) [+2.00000x] RAM Hits: 19|20 (-5.00000%) [-1.05263x] Total read+write: 30696|338852 (-90.9412%) [-11.0390x] Estimated Cycles: 31350|339536 (-90.7668%) [-10.8305x]play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 218|7072 (-96.9174%) [-32.4404x] L1 Hits: 273|7128 (-96.1700%) [-26.1099x] LL Hits: 1|1 (No change) RAM Hits: 10|9 (+11.1111%) [+1.11111x] Total read+write: 284|7138 (-96.0213%) [-25.1338x] Estimated Cycles: 628|7448 (-91.5682%) [-11.8599x]play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 332|183930316 (-99.9998%) [ -554007x] L1 Hits: 387|183930372 (-99.9998%) [ -475272x] LL Hits: 1|1 (No change) RAM Hits: 10|9 (+11.1111%) [+1.11111x] Total read+write: 398|183930382 (-99.9998%) [ -462137x] Estimated Cycles: 742|183930692 (-99.9996%) [ -247885x]
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45459sOh, uau! Nosso benchmark de 100 está caindo 11% e nosso benchmark de 1_000_000 está mais de 200.000x abaixo! De 183.930.692 ciclos estimados para 742 ciclos estimados!
Isso é uma redução de 99,9996%!
🐰 Ei, pelo menos pegamos esse bug de performance antes de chegar à produção… ah, é mesmo. Deixa pra lá…
Detecte Regressões de Desempenho em CI
Os executivos não ficaram felizes com a enxurrada de críticas negativas que nosso jogo recebeu devido ao meu pequeno bug de desempenho. Eles me disseram para não deixar isso acontecer de novo, e quando perguntei como, eles simplesmente me disseram para não fazê-lo novamente. Como eu deveria gerenciar isso‽
Felizmente, encontrei esta incrível ferramenta open source chamada Bencher. Existe um nível gratuito super generoso, então posso apenas usar Bencher Cloud para meus projetos pessoais. E no trabalho, onde tudo precisa estar em nossa nuvem privada, comecei a usar Bencher Auto-Hospedado.
Bencher tem adaptadores integrados, por isso é fácil de integrar ao CI. Após seguir o guia Rápido Início, consegui executar meus benchmarks e rastreá-los com o Bencher.
$ bencher run --project game "cargo bench" Finished `bench` profile [optimized + debuginfo] target(s) in 0.73s Running benches/play_game.rs (target/release/deps/play_game-84c12f98b1991829)play_game::bench_play_game_group::bench_play_game_100 Instructions: 23679|23679 (No change) L1 Hits: 30675|30675 (No change) LL Hits: 2|2 (No change) RAM Hits: 19|19 (No change) Total read+write: 30696|30696 (No change) Estimated Cycles: 31350|31350 (No change)play_game::bench_play_game_group::bench_play_game play_0:(100) Instructions: 218|218 (No change) L1 Hits: 273|273 (No change) LL Hits: 1|1 (No change) RAM Hits: 10|10 (No change) Total read+write: 284|284 (No change) Estimated Cycles: 628|628 (No change)play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) Instructions: 332|332 (No change) L1 Hits: 387|387 (No change) LL Hits: 1|1 (No change) RAM Hits: 10|10 (No change) Total read+write: 398|398 (No change) Estimated Cycles: 742|742 (No change)
Gungraun result: Ok. 3 without regressions; 0 regressed; 0 filtered; 3 benchmarks finished in 0.45370s
Bencher New Report:...View results:- play_game::bench_play_game_group::bench_play_game play_0:(100) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_0:(100) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c10f90a6-268a-4b31-b625-66f95eb4861f&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game play_1:(1_000_000) (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=c0c16a00-5ad1-4787-92ac-a39eed8c5375&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Estimated Cycles): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=f03d9a6c-2b63-45c3-b34a-37149d1a7961&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Instructions): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=17acf657-735b-4ece-ab32-ba857db5edce&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (L1 Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=009a129f-4476-4202-9e2b-cd7aed7110ac&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (LL Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=932a00d1-e064-4f18-81fb-aa94a5f6d5a0&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (RAM Hits): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=c98672c7-8229-4e90-9773-482618b71dbf&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409- play_game::bench_play_game_group::bench_play_game_100 (Total read+write): https://bencher.dev/console/projects/game/perf/game?branches=b53281dd-375a-4986-8074-17e9d488815e&heads=77345ed9-2e45-4d43-8186-f0f99b8120d1&testbeds=ef809413-f1ae-4889-bb91-d5e2e5769830&specs=%2C&benchmarks=4da8a40c-2282-487c-bac8-21218deba041&measures=0bd6ec91-2b29-47ea-801e-dc09338f3119&start_time=1773186232000&end_time=1775778233000&report=c703a61c-46a4-43bf-bbce-cb69d679b409Usando este incrível dispositivo de viagem no tempo que um simpático coelho me deu, consegui voltar ao passado e reviver o que teria acontecido se estivéssemos usando o Bencher desde o início. Você pode ver onde fizemos pela primeira vez o push da implementação bugada de FizzBuzzFibonacci. Imediatamente recebi falhas no CI como um comentário na minha solicitação de pull. No mesmo dia, corrigi o bug de desempenho, eliminando aquele loop extra e desnecessário. Sem incêndios. Apenas usuários felizes.
Bencher: Benchmarking Contínuo
Bencher é um conjunto de ferramentas de benchmarking contínuo. Já teve algum impacto de regressão de desempenho nos seus usuários? Bencher poderia ter prevenido isso. Bencher permite que você detecte e previna regressões de desempenho antes que sejam mescladas.
- Execute: Execute seus benchmarks localmente ou no CI usando os exatos mesmos runners bare metal e suas ferramentas de benchmarking favoritas. O CLI
bencherorquestra a execução dos seus benchmarks em bare metal e armazena seus resultados. - Rastreie: Acompanhe os resultados de seus benchmarks ao longo do tempo. Monitore, consulte e faça gráficos dos resultados usando o console web do Bencher baseado na branch de origem, testbed e medida.
- Capture: Capture regressões de desempenho localmente ou no CI usando o exato mesmo hardware bare metal. Bencher usa análises personalizáveis e de última geração para detectar regressões de desempenho antes que sejam mescladas.
Pelos mesmos motivos que os testes de unidade são executados para prevenir regressões de funcionalidades, benchmarks deveriam ser executados com o Bencher para prevenir regressões de desempenho. Bugs de desempenho são bugs!
Comece a capturar regressões de desempenho — experimente o Bencher Cloud gratuitamente.