JMH で Java のコードのベンチマークを取る

java

JMH (Java Microbenchmark Harness) は Java のベンチマークツール。 小さなコードを単純に実行するとコードが大きいときには行われない JIT コンパイルなどの最適化がはたらくことで実際よりパフォーマンスが高く出ることがあるが、 JMH はこれを防ぐことでより正確にベンチマークを取ることができるそうだ。

README に従いプロジェクトを生成しベンチマークを実行してみる。

$ mvn archetype:generate \
  -DinteractiveMode=false \
  -DarchetypeGroupId=org.openjdk.jmh \
  -DarchetypeArtifactId=jmh-java-benchmark-archetype \
  -DgroupId=org.sample \
  -DartifactId=test \
  -Dversion=1.0

$ tree
    .
├── aaaa
└── test
    ├── pom.xml
    └── src
        └── main
            └── java
                └── org
                    └── sample
                        └── MyBenchmark.java
           
$ mvn clean verify
$ java -jar target/benchmarks.jar
...
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.MyBenchmark.testMethod

# Run progress: 0.00% complete, ETA 00:08:20
# Fork: 1 of 5
# Warmup Iteration   1: 3341043347.302 ops/s
# Warmup Iteration   2: 3413334610.874 ops/s
# Warmup Iteration   3: 3435333707.240 ops/s
# Warmup Iteration   4: 3411134300.511 ops/s
# Warmup Iteration   5: 3357532714.229 ops/s
Iteration   1: 3400156491.349 ops/s
Iteration   2: 3386226994.749 ops/s
Iteration   3: 3447242449.844 ops/s
Iteration   4: 3445778472.937 ops/s
Iteration   5: 3445982939.981 ops/s

# Run progress: 20.00% complete, ETA 00:06:41
# Fork: 2 of 5
...
Benchmark                Mode  Cnt           Score          Error  Units
MyBenchmark.testMethod  thrpt   25  3392960046.446 ± 27900374.591  ops/s

メソッドに @Benchmark を付けると @BenchmarkMode のベンチマークが行われ @OutputTimeUnit の単位で出力される。 @State で状態を持つことができ @Setup(Level.Trial) すると fork ごとに一度だけ初期化される。 値が参照されていないために dead code として取り除かれないようにするには return するか Blackhole.consume() に渡せば良い。

package org.sample;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;

public class MyBenchmark2 {
    
    @State(Scope.Thread)
    public static class MyState {
        public int v;
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public void some(Blackhole blackhole, MyState state) {
        state.v = state.v * -1 + 1;
        String notusedValue = "" + state.v;
        blackhole.consume(notusedValue);
    }
}

必要な dependency は次の 2 つで、ビルドすると org.openjdk.jmh.Main を main class とする uber-jar が生成されるようになっている。

<dependencies>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>${jmh.version}</version>
    </dependency>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>${jmh.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
...
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <finalName>${uberjar.name}</finalName>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>org.openjdk.jmh.Main</mainClass>
                    </transformer>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                </transformers>
                <filters>
                    <filter>
                        <!--
                            Shading signed JARs will fail without this.
                            http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
                        -->
                        <artifact>*:*</artifact>
                        <excludes>
                            <exclude>META-INF/*.SF</exclude>
                            <exclude>META-INF/*.DSA</exclude>
                            <exclude>META-INF/*.RSA</exclude>
                        </excludes>
                    </filter>
                </filters>
            </configuration>
        </execution>
    </executions>
</plugin>

あるいはクラスパスを指定して直接呼び出すこともできる。

<!-- mvn exec:exec -->
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <executable>java</executable>
        <arguments>
            <argument>-classpath</argument>
            <classpath />
            <argument>org.openjdk.jmh.Main</argument>
        </arguments>
    </configuration>
</plugin>

参考

JMH - Java Microbenchmark Harness