Como faço para escrever um micro-benchmark correto em Java?
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 para se pensar.
Exemplo: O benchmark deve medir o tempo / iteração ou iterações / tempo e por quê?
Relacionado: O benchmarking do cronômetro é aceitável?
Respostas
Dicas sobre como escrever micro benchmarks dos criadores do Java HotSpot :
Regra 0: Leia um artigo confiável sobre JVMs e micro-benchmarking. Um bom é Brian Goetz, 2005 . Não espere muito de micro-benchmarks; eles medem apenas uma faixa limitada de características de desempenho da JVM.
Regra 1: Sempre inclua uma fase de aquecimento que execute seu kernel de teste por completo, o suficiente para acionar todas as inicializações e compilações antes das fases de tempo. (Menos iterações estão OK na fase de aquecimento. A regra é várias dezenas de milhares de iterações de loop interno.)
Regra 2: Sempre executar com -XX:+PrintCompilation
, -verbose:gc
etc., 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: Imprimir mensagens no início e no final das fases de cronometragem e aquecimento, para que você possa verificar se não há saída da Regra 2 durante a fase de cronometragem.
Regra 3: Esteja ciente da diferença entre -client
e -server
, e OSR e compilações regulares. A -XX:+PrintCompilation
bandeira 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 o servidor ao cliente, e regular ao OSR, se você deseja o melhor desempenho.
Regra 4: Esteja ciente dos efeitos de inicialização. Não imprima pela primeira vez durante a fase de temporização, pois a impressão carrega e inicializa classes. Não carregue novas classes fora da fase de aquecimento (ou fase de relatório final), a menos que esteja testando o carregamento de classes especificamente (e, nesse caso, carregue apenas as classes de teste). A regra 2 é sua primeira linha de defesa contra tais efeitos.
Regra 5: Esteja ciente dos efeitos de desotimização e recompilação. Não siga nenhum caminho de código pela primeira vez na fase de temporização, porque o compilador pode 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.
Regra 6: Use ferramentas apropriadas para ler a mente do compilador e espere ser surpreendido pelo código que ele produz. Inspecione o código 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 valores discrepantes. Use -Xbatch
para serializar o compilador com o aplicativo e considere a configuração -XX:CICompilerCount=1
para evitar que o compilador seja executado em paralelo consigo mesmo. Faça o possível para reduzir a sobrecarga do GC, defina Xmx
(grande o suficiente) igual Xms
e use UseEpsilonGCse estiver disponível.
Regra 8: Use uma biblioteca para seu benchmark, pois provavelmente é mais eficiente e já foi depurada para este propósito único. Como JMH , Caliper ou Benchmarks UCSD excelentes de Bill e Paul para Java .
Sei que esta pergunta foi marcada como respondida, mas gostaria de mencionar duas bibliotecas que nos ajudam a escrever micro benchmarks
Caliper do Google
Tutoriais de introdução
- http://codingjunkie.net/micro-benchmarking-with-caliper/
- http://vertexlabs.co.uk/blog/caliper
JMH do OpenJDK
Tutoriais de introdução
- Evitando armadilhas de benchmarking na JVM
- Usando JMH para Microbenchmarking Java
- Introdução ao JMH
Coisas importantes para benchmarks Java são:
- Aqueça o JIT primeiro executando o código várias vezes antes de cronometrá- lo
- 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 as iterações, é uma boa ideia executá-lo entre os 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 seja uma coleta de 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 de forma 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 escrevendo um blog sobre o design de uma estrutura de benchmarking em .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.
jmh é uma adição recente ao OpenJDK e foi escrito por alguns engenheiros de desempenho da Oracle. Certamente, vale a pena dar uma olhada.
O jmh é um harness Java para construir, executar e analisar benchmarks nano / micro / macro escritos em Java e outras linguagens voltadas para a JVM.
Informações muito interessantes enterradas nos comentários dos testes de amostra .
Veja também:
- Evitando armadilhas de benchmarking na JVM
- Discussão sobre os principais pontos fortes do jmh .
O benchmark deve medir o 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.
Se você está tentando comparar dois algoritmos, faça pelo menos dois benchmarks para cada um, alternando a ordem. ie:
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 perceptíveis (5-10% às vezes) no tempo de execução do mesmo algoritmo em passagens diferentes.
Além disso, certifique-se de que n seja muito grande, de modo que o tempo de execução de cada loop seja de pelo menos 10 segundos ou mais. Quanto mais iterações, mais números significativos em seu tempo de referência e mais confiáveis são os dados.
Certifique-se de usar de alguma forma os resultados calculados no código de referência. Caso contrário, seu código pode ser otimizado.
Existem muitas armadilhas possíveis para escrever micro-benchmarks em Java.
Primeiro: você tem que calcular com todos os tipos de eventos que levam tempo mais ou menos aleatórios: coleta de lixo, efeitos de cache (de SO para arquivos e de CPU para memória), IO etc.
Segundo: você não pode confiar na precisão dos tempos medidos para intervalos muito curtos.
Terceiro: O JVM otimiza seu código durante a execução. Assim, diferentes execuções na mesma instância JVM se tornarão cada vez mais rápidas.
Minhas recomendações: Faça seu benchmark rodar alguns segundos, o que é mais confiável do que um runtime de milissegundos. Aquecer a JVM (significa executar o benchmark pelo menos uma vez sem medir, para que a JVM possa executar otimizações). E execute seu benchmark várias vezes (talvez 5 vezes) e pegue o valor médio. Execute cada micro-benchmark em uma nova instância JVM (chame para cada benchmark novo Java), caso contrário, os efeitos de otimização do JVM podem influenciar a execução de testes posteriores. Não execute coisas que não são executadas na fase de aquecimento (pois isso poderia acionar o carregamento de classe e a recompilação).
Também deve ser observado 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 A
pode ser mais rápida durante a maioria das execuções do benchmark do que a implementação B
. Mas A
também pode ter um spread maior, de modo que o benefício de desempenho medido de A
não será significativo quando comparado com B
.
Portanto, também é importante escrever e executar um micro benchmark corretamente, mas também analisá-lo corretamente.
Para complementar o outro conselho excelente, também estaria atento ao seguinte:
Para algumas CPUs (por exemplo, gama Intel Core i5 com TurboBoost), a temperatura (e o número de núcleos atualmente em uso, bem como sua porcentagem de utilização) afeta a velocidade do clock. Como as CPUs são sincronizadas dinamicamente, isso pode afetar seus resultados. Por exemplo, se você tiver um aplicativo de thread único, a velocidade máxima do clock (com TurboBoost) é maior do que para um aplicativo que usa todos os núcleos. Isso pode, portanto, interferir nas comparações de desempenho único e multithread em alguns sistemas. Lembre-se de que a temperatura e a volatilidade também afetam por quanto tempo a frequência do Turbo é mantida.
Talvez um aspecto mais fundamentalmente importante sobre o qual você tenha controle direto: certifique-se de que está medindo a coisa certa! Por exemplo, se você estiver usando System.nanoTime()
para comparar um determinado código de referência, coloque as chamadas para a atribuição em lugares 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 imediatamente o horário de término 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");
http://opt.sourceforge.net/Java Micro Benchmark - tarefas de controle necessárias para determinar as características comparativas de desempenho do sistema de computador em diferentes plataformas. Pode ser usado para orientar as decisões de otimização e para comparar diferentes implementações Java.