Como escrevo um micro-benchmark correto em Java?

870

Como você escreve (e executa) um micro-benchmark correto em Java?

Estou procurando alguns exemplos de código e comentários que ilustram várias coisas em que pensar.

Exemplo: o benchmark deve medir tempo / iteração ou iterações / tempo e por quê?

Relacionado: O comparador de cronômetro é aceitável?

John Nilsson
fonte
Veja [esta pergunta] [1] de alguns minutos atrás para obter informações relacionadas. edit: desculpe, isso não deveria ser uma resposta. Eu deveria ter postado como um comentário. [1]: stackoverflow.com/questions/503877/…
Tiago
Foi depois de planejar encaminhar o pôster dessa pergunta para uma pergunta como essa que notei que essa pergunta não existia. Então aqui está, espero que ajude algumas boas dicas ao longo do tempo.
John John Nilsson
5
Java 9 pode fornecer algumas características para-aferição micro: openjdk.java.net/jeps/230
Raedwald
1
@Raedwald Eu acho que que visa PEC para adicionar algum micro referência ao código JDK, mas eu não acho que jmh será incluído no JDK ...
assylias
1
@Raedwald Olá do futuro. Não fez o corte .
Michael

Respostas:

787

Dicas sobre como escrever micro benchmarks dos criadores do Java HotSpot :

Regra 0: Leia um artigo respeitável sobre JVMs e micro-benchmarking. Uma boa é Brian Goetz, 2005 . Não espere muito dos micro-benchmarks; eles medem apenas um intervalo limitado de características de desempenho da JVM.

Regra 1: Sempre inclua uma fase de aquecimento que execute o kernel de teste até o fim, o suficiente para acionar todas as inicializações e compilações antes da temporização das fases. (Menos iterações são boas na fase de aquecimento. A regra geral é várias dezenas de milhares de iterações do loop interno.)

Regra 2: Sempre executar com -XX:+PrintCompilation, -verbose:gcetc., para que possa verificar se o compilador e outras partes da JVM não estão fazendo um trabalho inesperado durante a sua fase de timing.

Regra 2.1: Imprima mensagens no início e no final das fases de temporização e aquecimento, para que você possa verificar se não há saída da Regra 2 durante a fase de temporização.

Regra 3: Esteja ciente da diferença entre-client e -server, e OSR e compilações regulares. A -XX:+PrintCompilationbandeira relata compilações OSR com um sinal de arroba para denotar o ponto de entrada não-inicial, por exemplo: Trouble$1::run @ 2 (41 bytes). Prefira servidor ao cliente e regular ao OSR, se você estiver buscando o melhor desempenho.

Regra 4: Esteja ciente dos efeitos de inicialização. Não imprima pela primeira vez durante sua fase de temporização, pois a impressão carrega e inicializa as classes. Não carregue novas classes fora da fase de aquecimento (ou fase final do relatório), a menos que você esteja testando o carregamento da classe especificamente (e, nesse caso, carregue apenas as classes de teste). A regra 2 é sua primeira linha de defesa contra tais efeitos.

Artigo 5: Esteja ciente dos efeitos de desoptimização e recompilação. Não pegue nenhum caminho de código pela primeira vez na fase de temporização, porque o compilador pode colocar lixo e recompilar o código, com base em uma suposição otimista anterior de que o caminho não seria usado. A regra 2 é sua primeira linha de defesa contra tais efeitos.

Artigo 6: Use as ferramentas apropriadas para ler a mente do compilador e espere ser surpreendido pelo código que ele produz. Inspecione o código você mesmo antes de formar teorias sobre o que torna algo mais rápido ou mais lento.

Regra 7: Reduza o ruído em suas medições. Execute seu benchmark em uma máquina silenciosa e execute-o várias vezes, descartando discrepâncias. Use -Xbatchpara serializar o compilador com o aplicativo e considere a configuração -XX:CICompilerCount=1para impedir que o compilador seja executado em paralelo consigo mesmo. Tente o seu melhor para reduzir a sobrecarga do GC, definir Xmx(suficientemente grande) iguais Xmse usarUseEpsilonGC se estiver disponível.

Regra 8: use uma biblioteca para seu benchmark, pois provavelmente é mais eficiente e já foi depurado para esse único propósito. Tais como JMH , Caliper ou Bill e Paul's Excellent UCSD Benchmarks para Java .

Eugene Kuleshov
fonte
5
Este também foi um artigo interessante: ibm.com/developerworks/java/library/j-jtp12214
John Nilsson
142
Além disso, nunca use System.currentTimeMillis (), a menos que você esteja bem com precisão de + ou - 15 ms, o que é típico na maioria das combinações OS + JVM. Use System.nanoTime () em vez disso.
22611 Scott Carey
5
Algum artigo de javaOne: azulsystems.com/events/javaone_2009/session/…
bestsss
93
Note-se que System.nanoTime()não é garantido que seja mais preciso do que System.currentTimeMillis(). Só é garantido que seja pelo menos tão preciso. Geralmente, porém, é substancialmente mais preciso.
Gravity
41
A principal razão pela qual se deve usar, em System.nanoTime()vez de, System.currentTimeMillis()é que o primeiro tem a garantia de aumentar monotonicamente. Subtrair os valores retornados duas currentTimeMillisinvocações pode realmente dar resultados negativos, possivelmente porque a hora do sistema foi ajustada por algum daemon NTP.
21315 Waldheinz
239

Eu sei que esta pergunta foi marcada como respondida, mas eu queria mencionar duas bibliotecas que nos ajudam a escrever micro benchmarks

Pinça do Google

Tutoriais de introdução

  1. http://codingjunkie.net/micro-benchmarking-with-caliper/
  2. http://vertexlabs.co.uk/blog/caliper

JMH do OpenJDK

Tutoriais de introdução

  1. Evitando armadilhas de benchmarking na JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/
Aravind Yarram
fonte
37
+1 poderia ter sido adicionado como Regra 8 da resposta aceita: Regra 8: como muitas coisas podem dar errado, você provavelmente deve usar uma biblioteca existente em vez de tentar fazer isso sozinho!
Assilias 6/12/12
8
@Pangea jmh é provavelmente superior a Caliper hoje em dia, Veja também: groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/...
assylias
86

As coisas importantes para os benchmarks Java são:

  • Aqueça o JIT primeiro executando o código várias vezes antes do tempo que
  • Certifique-se de executá-lo por tempo suficiente para poder medir os resultados em segundos ou (melhor) dezenas de segundos
  • Embora você não possa chamar System.gc()entre iterações, é uma boa ideia executá-lo entre testes, para que cada teste tenha um espaço de memória "limpo" para trabalhar. (Sim, gc()é mais uma dica do que uma garantia, mas é muito provável que realmente colete lixo na minha experiência.)
  • Eu gosto de exibir iterações e tempo, e uma pontuação de tempo / iteração que pode ser escalada para que o "melhor" algoritmo obtenha uma pontuação de 1,0 e outros sejam pontuados de maneira relativa. Isso significa que você pode executar todos os algoritmos por um longo tempo, variando o número de iterações e o tempo, mas ainda obtendo resultados comparáveis.

Estou apenas no processo de criar um blog sobre o design de uma estrutura de benchmarking no .NET. Eu tenho um par de posts anteriores , que pode ser capaz de lhe dar algumas idéias - nem tudo será apropriado, é claro, mas alguns dos que seja.

Jon Skeet
fonte
3
Nitpick menor: IMO "para que cada teste seja" deve ser "para que cada teste seja", pois o primeiro dá a impressão de que a chamada gc sempre libera memória não utilizada.
precisa saber é o seguinte
@ SanjayT.Sharma: Bem, a intenção é que realmente funcione . Embora não seja estritamente garantido, na verdade é uma dica bastante forte. Editará para ficar mais claro.
amigos estão dizendo sobre jon
1
Não concordo em chamar System.gc (). É uma dica, só isso. Nem mesmo "espero que faça alguma coisa". Você nunca deve chamá-lo. Isso é programação, não arte.
gyorgyabraham
13
@gyabraham: Sim, é uma dica - mas é uma que eu observei que geralmente é usada. Então, se você não gosta de usar System.gc(), como propõe minimizar a coleta de lixo em um teste devido a objetos criados em testes anteriores? Sou pragmático, não dogmático.
Jon Skeet
9
@gyabraham: Eu não sei o que você quer dizer com "grande retorno". Você pode elaborar, e novamente - você tem uma proposta para obter melhores resultados? Eu fiz explicitamente dizer que não é uma garantia ...
Jon Skeet
48

jmh é uma adição recente ao OpenJDK e foi escrita por alguns engenheiros de desempenho da Oracle. Certamente vale a pena dar uma olhada.

O jmh é um equipamento Java para construção, execução e análise de benchmarks nano / micro / macro escritos em Java e outras linguagens direcionadas à JVM.

Informações muito interessantes enterradas em comentários dos testes de amostra .

Veja também:

assilias
fonte
1
Consulte também esta postagem do blog: psy-lob-saw.blogspot.com/2013/04/… para obter detalhes sobre como começar o JMH.
Nitsan Wakart
FYI, JEP 230: Microbenchmark Suite é uma proposta do OpenJDK baseada neste projeto Java Microbenchmark Harness (JMH) . Não fez o corte para o Java 9, mas pode ser adicionado posteriormente.
Basil Bourque 01/07
23

O benchmark deve medir tempo / iteração ou iterações / tempo e por quê?

Depende do que você está tentando testar.

Se você estiver interessado em latência , use tempo / iteração e se estiver interessado em taxa de transferência , use iterações / tempo.

Peter Lawrey
fonte
16

Se você estiver tentando comparar dois algoritmos, faça pelo menos dois benchmarks para cada um, alternando a ordem. ou seja:

for(i=1..n)
  alg1();
for(i=1..n)
  alg2();
for(i=1..n)
  alg2();
for(i=1..n)
  alg1();

Eu encontrei algumas diferenças visíveis (5-10% às vezes) no tempo de execução do mesmo algoritmo em passes diferentes.

Além disso, verifique se n é muito grande, para que o tempo de execução de cada loop seja de no mínimo 10 segundos. Quanto mais iterações, mais números significativos no tempo de referência e mais confiáveis ​​esses dados.

Kip
fonte
5
Alterar naturalmente a ordem influencia o tempo de execução. As otimizações da JVM e os efeitos de armazenamento em cache vão funcionar aqui. Melhor é "aquecer" a otimização da JVM, realizar várias execuções e comparar todos os testes em uma JVM diferente.
Mnementh
15

De alguma forma, use resultados que são computados no código de referência. Caso contrário, seu código poderá ser otimizado.

Peter Štibraný
fonte
13

Existem muitas armadilhas possíveis para escrever micro-benchmarks em Java.

Primeiro: você deve calcular com todos os tipos de eventos que levam tempo mais ou menos aleatório: coleta de lixo, efeitos de cache (do sistema operacional para arquivos e da CPU para memória), E / S

Segundo: você não pode confiar na precisão dos tempos medidos por intervalos muito curtos.

Terceiro: a JVM otimiza seu código durante a execução. Portanto, execuções diferentes na mesma instância da JVM se tornarão cada vez mais rápidas.

Minhas recomendações: Faça seu benchmark executar alguns segundos, mais confiável que um tempo de execução em milissegundos. Aqueça a JVM (significa executar o benchmark pelo menos uma vez sem medir, para que a JVM possa executar otimizações). Execute seu benchmark várias vezes (talvez 5 vezes) e aceite o valor mediano. Execute todos os micro-benchmarks em uma nova instância da JVM (chame cada novo Java de benchmark), caso contrário, os efeitos de otimização da JVM podem influenciar os testes posteriores. Não execute coisas que não são executadas na fase de aquecimento (pois isso pode disparar o carregamento da classe e a recompilação).

Mnementh
fonte
8

Deve-se notar também que também pode ser importante analisar os resultados do micro benchmark ao comparar diferentes implementações. Portanto, um teste de significância deve ser feito.

Isso ocorre porque a implementação Apode ser mais rápida durante a maioria das execuções do benchmark do que a implementação B. Mas Atambém pode ter um spread mais alto; portanto, o benefício medido do desempenho Anão terá qualquer significado quando comparado comB .

Portanto, também é importante escrever e executar um micro benchmark corretamente, mas também analisá-lo corretamente.

SpaceTrucker
fonte
8

Para adicionar aos outros excelentes conselhos, eu também estaria atento ao seguinte:

Para algumas CPUs (por exemplo, a gama Intel Core i5 com TurboBoost), a temperatura (e o número de núcleos atualmente em uso, bem como a porcentagem de utilização) afetam a velocidade do relógio. Como as CPUs têm um clock dinâmico, isso pode afetar seus resultados. Por exemplo, se você tiver um aplicativo de thread único, a velocidade máxima do relógio (com TurboBoost) é maior do que para um aplicativo que usa todos os núcleos. Portanto, isso pode interferir nas comparações de desempenho único e multithread em alguns sistemas. Lembre-se de que a temperatura e as oscilações também afetam por quanto tempo a frequência Turbo é mantida.

Talvez um aspecto mais fundamentalmente importante sobre o qual você tenha controle direto: verifique se está medindo a coisa certa! Por exemplo, se você estiver usando System.nanoTime()para comparar um determinado código, faça as chamadas para a tarefa em locais que façam sentido para evitar medir coisas nas quais você não está interessado. Por exemplo, não faça:

long startTime = System.nanoTime();
//code here...
System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds");

O problema é que você não está obtendo o horário final imediatamente quando o código termina. Em vez disso, tente o seguinte:

final long endTime, startTime = System.nanoTime();
//code here...
endTime = System.nanoTime();
System.out.println("Code took "+(endTime-startTime)+"nano seconds");
Sina Madani
fonte
Sim, é importante não fazer um trabalho não relacionado dentro da região temporizada, mas seu primeiro exemplo ainda está bom. Há apenas uma chamada para println, não uma linha de cabeçalho separada ou algo assim, e System.nanoTime()deve ser avaliada como a primeira etapa na construção da string arg para essa chamada. Não há nada que um compilador possa fazer com o primeiro que ele não possa fazer com o segundo, e nenhum deles os encoraja a fazer um trabalho extra antes de registrar um tempo de parada.
Peter Cordes
7

http://opt.sourceforge.net/ Java Micro Benchmark - controle as tarefas necessárias para determinar as características de desempenho comparativas do sistema de computador em diferentes plataformas. Pode ser usado para orientar decisões de otimização e comparar diferentes implementações Java.

Yuriy
fonte
2
Parece apenas comparar o hardware da JVM +, não uma parte arbitrária do código Java.
27612 Stefan L