Por que um número só não é uma medição

Prévia grátis 5 min de leitura

Aqui está um benchmark. Ele aplica upcase em vinte mil strings e cronometra com o relógio que todo mundo pega primeiro.

require "benchmark"
WORDS = (1..20_000).map { |i| "row-#{i}" }
def build(words) = words.map(&:upcase)

5.times do |i|
  t = Time.now
  build(WORDS)
  puts "run #{i + 1}: #{((Time.now - t) * 1000).round(2)} ms"
end

Rode três vezes seguidas, na mesma máquina e no mesmo minuto, sem começar nem parar nenhum outro trabalho. Aqui está uma máquina com Ruby 4.0.1:

execução 1:  4.24  3.93  3.35  3.48  2.91 ms
execução 2:  2.19  2.12  1.87  2.03  1.87 ms
execução 3:  1.93  1.93  1.71  1.86  1.80 ms

O número mais lento é quase 2,5 vezes o mais rápido, e o código nunca mudou. Suponha que você rodou isso uma vez e reportou 4.24 ms, e um colega rodou uma vez e reportou 1.71 ms. Os dois estariam falando a verdade, e discordariam por um fator de dois.

O que o relógio mediu

Uma diferença de Time.now sozinha mede o humor da sua máquina, e esse humor tem partes com nome.

O aquecimento vem primeiro. A primeira passada por build paga por falhas no cache de métodos e pela montagem dos inline caches, mais a compilação se o YJIT estiver ligado. Repare no formato dentro de cada execução acima: a run 1 é a mais lenta, e os números vão assentando para baixo. Isso é tendência, e não ruído, e se repete.

Depois vem o carregamento preguiçoso. A primeira chamada num caminho de código puxa constantes com autoload, abre conexões e aloca os buffers que o caminho vai reusar dali em diante. Você pagou um custo único e o cobrou do algoritmo.

Há também o coletor de lixo. O Ruby roda o GC quando a pressão de alocação manda, e não quando a sua medição acaba. Se uma coleta dispara dentro da sua janela, você cronometrou uma coleta. Se dispara logo depois, você cronometrou uma mentira na direção oposta.

Por último vem o resto da máquina: escalonamento de frequência da CPU, limite térmico, outro processo acordando e o sistema operacional levando as suas threads para outro núcleo. Nada disso pede a sua permissão.

É essa a variação entre as três execuções acima. É por isso que a diferença entre execuções é maior que a diferença entre a primeira e a última run dentro de uma delas.

A afirmação que você está fazendo

Quando você diz "esta versão é mais rápida", está fazendo uma afirmação que alguém vai testar num review. Há três maneiras de perder essa discussão, e as três são evitáveis.

Você mediu um custo de aquecimento e chamou de algoritmo. Você mediu uma vez e pegou um número da cauda da distribuição. Você comparou duas implementações cuja diferença real é menor que o ruído da sua máquina.

Cada uma tem uma correção, e nenhuma é complicada. São passos que a versão de uma linha com Time.now pula.

O curso anterior, e este

O curso de nível Sênior "Performance: notação O" terminou dizendo o que não conseguia contar. Ele disse em qual curva você está: o formato que o seu custo segue conforme a entrada cresce. Nunca disse onde nessa curva você está agora, porque curva é previsão e previsão não é medição.

Esta é a metade da medição. A complexidade disse do que desconfiar. O benchmarking diz se você estava certo, e com mais frequência do que um júnior espera, a resposta é que não estava.

O que este curso cobre

A lição 2 é aquecimento e regime estável. Ela explica por que o Benchmark.bmbm roda o seu código duas vezes e joga o primeiro resultado fora, e por que esse ensaio corrige um problema específico em vez de ser ritual.

A lição 3 é variância. Um número não é resultado. Você reporta uma faixa, e duas implementações cujas faixas se sobrepõem não são distinguíveis. Dizer que uma é mais rápida é uma afirmação que vão cobrar de você.

A lição 4 é iterações por segundo contra tempo decorrido, e por que a primeira é o padrão melhor para comparar dois candidatos. Ela também resolve o que está instalado aqui, porque essa resposta mudou no Ruby 4.0.1.

A lição 5 é Rails, onde a maioria dos benchmarks erra. Os culpados de sempre são banco frio, query cache, recarga de código, log ligado e um objeto que já tinha sido memoizado na linha de cima.

A lição 6 é a pergunta que vem antes de medir qualquer coisa: quanto do total esse candidato consegue tocar, no máximo? Ganhar 40% em 0,3% da requisição não é ganho.

A lição 7 transforma uma medição em padrão do time. Ela cobre orçamento de performance, onde o limite mora e o modo de falha honesto de testes de performance num runner de CI barulhento.

Comece pelo formato que você já viu: a run 1 é sempre a mais lenta. A lição 2 é sobre pagar esse custo de propósito, antes de o relógio começar.

Aula 1 de 7 · grátis

Continue com a aula 2

Aquecimento e regime estável

Explica como o ensaio do Benchmark.bmbm tira custos únicos, como reconhecer o regime estável e como o GC distorce leituras.

Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.

Digite para buscar...