Como fazer benchmark de código Rust com Iai


O que é Benchmarking?

Benchmarking é a prática de testar o desempenho do seu código para ver quão rápido (latência) ou quanta (vazão) trabalho ele pode realizar. Esta etapa frequentemente negligenciada no desenvolvimento de software é crucial para criar e manter códigos rápidos e de alto desempenho. O Benchmarking fornece as métricas necessárias para os desenvolvedores entenderem quão bem o seu código se comporta sob diversas cargas de trabalho e condições. Pelos mesmos motivos pelos quais você escreve testes unitários e de integração para prevenir regressões de recursos, você deve escrever benchmarks para prevenir regressões de desempenho. Bugs de desempenho são bugs!

O que é Rust?

Rust é uma linguagem de programação de código aberto que enfatiza velocidade, confiabilidade e produtividade. Consegue garantir a segurança da memória sem a necessidade de um coletor de lixo.

Você deve considerar o uso do Rust se estiver escrevendo um:

  • Programa de baixo nível onde o desempenho é importante
  • Biblioteca compartilhada que será usada por várias linguagens diferentes
  • Interface de Linha de Comando (CLI) complexa
  • Projeto de software de longa duração com muitos contribuidores

Rust tem uma forte ênfase na produtividade do desenvolvedor. Cargo é o gerenciador de pacotes oficial, e ele lida com várias tarefas, como:

  • Gerenciamento de dependências do projeto
  • Compilando binários, testes e benchmarks
  • Linting
  • Formatação

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 1 a 100 (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 1 a 100 inclusivamente.
  • Para cada número, calcule o módulo (resto depois da divisão) para ambos 3 e 5.
  • 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 é 0 para ambos 3 e 5 então imprima FizzBuzz.
  • Se o resto é 0 apenas para 3 então imprima Fizz.
  • Se o resto é 0 apenas para 5 então imprima Buzz.
  • 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.rs

Você 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`
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...
97
98
Fizz
Buzz

🐰 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.rs

Antes 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 de testes unitários e benchmarking integrado do Rust. Apesar de fazer parte da biblioteca padrão do Rust, libtest bench ainda é considerado instável, portanto, está disponível apenas em versões de compilador nightly. Para trabalhar no compilador Rust estável, um arnês de benchmarking separado precisa ser usado. No entanto, nenhum dos dois está sendo ativamente desenvolvido.

O arnês de benchmarking mais ativamente mantido dentro do ecossistema Rust é o Criterion. Ele funciona tanto em versões estáveis quanto nightly do compilador Rust, e se tornou o padrão de facto dentro da comunidade Rust. Criterion também é muito mais rico em recursos em comparação com libtest bench.

Uma alternativa experimental ao Criterion é o Iai, do mesmo criador do Criterion. No entanto, ele usa contagens de instrução em vez de tempo de clock de parede: instruções de CPU, acessos L1, acessos L2 e acessos à RAM. Isso permite a realização de benchmarks de tiro único, uma vez que essas métricas devem permanecer quase idênticas entre as execuções.

Todos três são suportados pelo Bencher. Então por que escolher o Iai? O Iai usa contagens de instrução em vez do tempo real. Isso o torna ideal para benchmark contínuo, ou seja, benchmarking em CI. Eu sugeriria usar o Iai para benchmark contínuo, especialmente se você está usando runners compartilhados. É importante entender que o Iai só mede uma aproximação do que você realmente se importa. Ir de 1.000 instruções para 2.000 instruções dobra a latência do seu aplicativo? Talvez sim, talvez não. Por isso, pode ser útil também executar benchmarks baseados no tempo real em paralelo com benchmarks baseados em contagens de instrução.

🐰 O Iai não tem atualização há mais de 3 anos. Então você pode considerar usar o Iai-Callgrind em vez dele.

Instale o Valgrind

O Iai usa uma ferramenta chamada Valgrind para coletar contagens de instrução. O Valgrind dá suporte ao Linux, Solaris, FreeBSD e MacOS. No entanto, o suporte ao MacOS está limitado aos processadores x86_64, já que os processadores arm64 (M1, M2, etc) ainda não são suportados.

No Debian use: sudo apt-get install valgrind

No MacOS (x86_64/Intel chip only): brew install valgrind

Refatorando o FizzBuzz

Para testar nosso aplicativo FizzBuzz, precisamos desacoplar nossa lógica da função main do programa. Os bancos de teste não conseguem benchmarkar a função main. Para fazer isso, precisamos fazer algumas alterações.

Em src, crie um novo arquivo chamado lib.rs:

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

Adicione o seguinte código em 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 número inteiro não assinado n, chama fizz_buzz com aquele número, e se print for true imprime o resultado.
  • fizz_buzz: Recebe um número inteiro não assinado n e executa a lógica real de Fizz, Buzz, FizzBuzz, ou número retornando o resultado como uma string.

Em seguida, atualize main.rs para ter esta aparência:

use game::play_game;
fn main() {
for i in 1..=100 {
play_game(i, true);
}
}
  • game::play_game: Importa play_game do pacote game que acabamos de criar com lib.rs.
  • main: O ponto de entrada principal em nosso programa que percorre os números de 1 a 100 inclusos e chama play_game para cada número, com print definido como true.

Fazendo benchmark do FizzBuzz

Para fazer o benchmark do nosso código, precisamos criar um diretório benches e adicionar um arquivo para conter nossos benchmarks, play_game.rs:

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

Dentro de play_game.rs, adicione o seguinte 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);
  • Importe a função play_game do nosso pacote game.
  • Crie uma função chamada bench_play_game.
  • Execute nosso macro-benchmark dentro de uma “caixa preta” para que o compilador não otimize nosso código.
  • Itere de 1 a 100 inclusivamente.
  • Para cada número, chame play_game, com print definido como false.

Agora, precisamos configurar o pacote game para executar nossos benchmarks.

Adicione o seguinte na parte inferior do seu arquivo Cargo.toml:

[dev-dependencies]
iai = "0.1"
[[bench]]
name = "play_game"
harness = false
  • iai: Adicione iai como uma dependência de desenvolvimento, já que estamos usando apenas para testes de desempenho.
  • bench: Registre play_game como um benchmark e defina harness como false, já que estaremos usando o Iai como nossa estrutura de benchmark.

Agora estamos prontos para fazer o benchmark do nosso código, execute 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 aumentar o ritmo! Temos nossas primeiras métricas de benchmark!

Finalmente, podemos descansar nossas cabeças cansadas de desenvolvedores… Brincadeira, nossos usuários querem um novo recurso!

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 1 a 100 (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_number que recebe um número inteiro sem sinal e retorna um booleano.
  • Itere para todos os números de 0 ao nosso número específico n inclusive.
  • Inicialize nossa sequência Fibonacci começando com 0 e 1 como os números anterior e atual respectivamente.
  • Itere enquanto o número atual for menor que a iteração atual i.
  • Adicione o número atual e anterior para obter o número próximo.
  • Atualize o número anterior para o número atual.
  • Atualize o número atual para o número próximo.
  • Uma vez que atual for maior ou igual ao número especifico n, nós sairemos do loop.
  • Verifique se o número atual é igual ao número especificado n e, se for, retorne true.
  • 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_buzz para fizz_buzz_fibonacci para torná-la mais descritiva.
  • Chame nossa função auxiliar is_fibonacci_number.
  • Se o resultado de is_fibonacci_number for true retorne Fibonacci.
  • Se o resultado de is_fibonacci_number for false, execute a mesma lógica Fizz, 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.

Fazendo benchmark do FizzBuzzFibonacci

Agora, podemos executar novamente nosso 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%)

Ah, legal! O Iai nos diz que a diferença entre os ciclos estimados dos nossos jogos FizzBuzz e FizzBuzzFibonacci é de +522.6091%. Seus números serão um pouco diferentes dos meus. No entanto, a diferença entre os dois jogos provavelmente está na faixa de 5x. Isso me parece bom! Especialmente para adicionar um recurso tão sofisticado quanto Fibonacci ao nosso jogo. A garotada vai 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 15 como entrada.
  • Finalmente, jogue nosso jogo com o novo inteiro não assinado i analisado.

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`
Fibonacci

E 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`
FizzBuzz

Nossa, que teste completo! O CI passou. Nossos chefes estão entusiasmados. Vamos lançá-lo! 🚀

O Fim


SpongeBob SquarePants Três Semanas Depois
Meme Está Tudo Bem

🐰 … 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:

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));
}
  • Um micro-benchmark bench_play_game_100 para jogar o jogo com o número cem (100)
  • Um micro-benchmark bench_play_game_1_000_000 para jogar o jogo com o número um milhão (1_000_000)

Quando eu executei, eu obtive isso:

$ 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

Espere por isso… espere por isso…

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

O quê! 6,685 ciclos estimados x 1,000 deveria ser 6,685,000 ciclos estimados e não 155,109,206 ciclos estimados 🤯 Apesar de ter acertado o código da minha sequência de Fibonacci funcionalmente, devo ter algum bug de desempenho 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 0 e 1 como os números anterior e atual, respectivamente.
  • Itere enquanto o número atual for menor que o número dado n.
  • Adicione o número anterior e atual para obter o número próximo.
  • Atualize o número anterior para o número atual.
  • Atualize o número atual para o número próximo.
  • Uma vez que atual seja maior ou igual ao número dado n, sairemos do loop.
  • Verifique se o número atual é igual ao número dado n e retorne esse resultado.

Agora vamos reexecutar esses benchmarks e ver como nos saímos:

$ 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%)

Uau! Nosso benchmark bench_play_game voltou ao patamar que estava para o FizzBuzz original. Eu gostaria de poder lembrar exatamente qual era essa pontuação. Já se passaram três semanas. Meu histórico do terminal não vai tão longe. E o Iai só compara com o resultado mais recente. Mas eu acho que está perto!

O benchmark bench_play_game_100 está quase 10 vezes menor, -87.22513%. E o benchmark bench_play_game_1_000_000 está mais de 10.000 vezes mais baixo! 155,109,206 ciclos estimados para 950 ciclos estimados! Isso é -99.99939%!

🐰 Ei, pelo menos pegamos esse bug de desempenho antes que ele chegasse à produção… ah, certo. Nem me lembrei…

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 [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 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

Bencher é um conjunto de ferramentas de benchmarking contínuas. 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 cheguem à produção.

  • Execute: Execute seus benchmarks localmente ou no CI usando suas ferramentas de benchmarking favoritas. O CLI bencher simplesmente envolve seu harness de benchmark existente 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 no CI. Bencher usa análises personalizáveis e de última geração para detectar regressões de desempenho antes que elas cheguem à produção.

Pelos mesmos motivos que os testes de unidade são executados no CI para prevenir regressões de funcionalidades, benchmarks deveriam ser executados no CI com o Bencher para prevenir regressões de desempenho. Bugs de desempenho são bugs!

Comece a capturar regressões de desempenho no CI — experimente o Bencher Cloud gratuitamente.

🤖 Este documento foi gerado automaticamente pelo OpenAI GPT-4. Pode não ser preciso e pode conter erros. Se você encontrar algum erro, abra um problema no GitHub.