Microbenchmarking com Java

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.