Por que um número só não é uma medição
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.
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.