Microbenchmarking com Java
1. Introdução
Este artigo rápido é focado no JMH (o Java Microbenchmark Harness). Isso foi adicionado ao JDK começando com o JDK 12; para versões anteriores, precisamos adicionar explicitamente as dependências aos nossos projetos.
Simplificando, o JMH cuida de coisas como aquecimento da JVM e caminhos de otimização de código, tornando o benchmarking o mais simples possível.
2. Começando
Para começar, podemos continuar trabalhando com o Java 8 e simplesmente definir as dependências:
org.openjdk.jmh
jmh-core
1.19
org.openjdk.jmh
jmh-generator-annprocess
1.19
As versões mais recentes deJMH CoreeJMH Annotation Processor podem ser encontradas no Maven Central.
Em seguida, crie um benchmark simples utilizando a anotação@Benchmark (em qualquer classe pública):
@Benchmark
public void init() {
// Do nothing
}
Em seguida, adicionamos a classe principal que inicia o processo de benchmarking:
public class BenchmarkRunner {
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
Agora, executarBenchmarkRunner executará nosso benchmark indiscutivelmente um tanto inútil. Após a conclusão da execução, é apresentada uma tabela de resumo:
# Run complete. Total time: 00:06:45
Benchmark Mode Cnt Score Error Units
BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops/s
3. Tipos de benchmarks
JMH suporta alguns benchmarks possíveis:Throughput,AverageTime,SampleTime, eSingleShotTime. Eles podem ser configurados por meio da anotação@BenchmarkMode:
@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void init() {
// Do nothing
}
A tabela resultante terá uma métrica de tempo médio (em vez de taxa de transferência):
# Run complete. Total time: 00:00:40
Benchmark Mode Cnt Score Error Units
BenchMark.init avgt 20 ≈ 10⁻⁹ s/op
4. Configurando aquecimento e execução
Usando a anotação@Fork, podemos definir como a execução do benchmark acontece: o parâmetrovalue controla quantas vezes o benchmark será executado, e o parâmetrowarmup controla quantas vezes um benchmark será executado a seco antes de os resultados serem coletados, por exemplo:
@Benchmark
@Fork(value = 1, warmups = 2)
@BenchmarkMode(Mode.Throughput)
public void init() {
// Do nothing
}
Isso instrui a JMH a executar dois garfos de aquecimento e descartar os resultados antes de passar para o benchmarking em tempo real.
Além disso, a anotação@Warmup pode ser usada para controlar o número de iterações de aquecimento. Por exemplo,@Warmup(iterations = 5) informa ao JMH que cinco iterações de aquecimento serão suficientes, ao contrário do padrão 20.
5. Estado
Vamos agora examinar como uma tarefa menos trivial e mais indicativa de benchmarking de um algoritmo de hashing pode ser realizada utilizandoState. Suponha que decidimos adicionar proteção extra contra ataques de dicionário em um banco de dados de senhas, hash a senha algumas centenas de vezes.
Podemos explorar o impacto no desempenho usando um objetoState:
@State(Scope.Benchmark)
public class ExecutionPlan {
@Param({ "100", "200", "300", "500", "1000" })
public int iterations;
public Hasher murmur3;
public String password = "4v3rys3kur3p455w0rd";
@Setup(Level.Invocation)
public void setUp() {
murmur3 = Hashing.murmur3_128().newHasher();
}
}
Nosso método de benchmark será semelhante a:
@Fork(value = 1, warmups = 1)
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void benchMurmur3_128(ExecutionPlan plan) {
for (int i = plan.iterations; i > 0; i--) {
plan.murmur3.putString(plan.password, Charset.defaultCharset());
}
plan.murmur3.hash();
}
Aqui, o campoiterations será preenchido com os valores apropriados da anotação@Param pelo JMH quando for passado para o método de referência. O método anotado@Setup é invocado antes de cada invocação do benchmark e cria um novoHasher garantindo o isolamento.
Quando a execução for concluída, obteremos um resultado semelhante ao abaixo:
# Run complete. Total time: 00:06:47
Benchmark (iterations) Mode Cnt Score Error Units
BenchMark.benchMurmur3_128 100 thrpt 20 92463.622 ± 1672.227 ops/s
BenchMark.benchMurmur3_128 200 thrpt 20 39737.532 ± 5294.200 ops/s
BenchMark.benchMurmur3_128 300 thrpt 20 30381.144 ± 614.500 ops/s
BenchMark.benchMurmur3_128 500 thrpt 20 18315.211 ± 222.534 ops/s
BenchMark.benchMurmur3_128 1000 thrpt 20 8960.008 ± 658.524 ops/s
5. Conclusão
Este tutorial enfocou e apresentou o micro arnês de benchmarking do Java.
Como sempre, os exemplos de código podem ser encontradoson GitHub.