プロジェクト

全般

プロフィール

JMH

はじめに

JMHは、Javaでマイクロベンチマーク(比較的短い処理に要する時間を測る)を行う枠組みです。
公式サイトは次です。
http://openjdk.java.net/projects/code-tools/jmh/

Javaでは、実行環境において、処理の実行速度に様々な影響があり、1回の処理時間を計測するだけでは性能を的確に把握することができません。
影響としては、JITコンパイラの特性(最初はバイトコードをインタプリターで実行し、ある閾値を超える回数処理が実行されると、CPUネイティブコードに実行時にコンパイルし、以降その処理はCPUネイティブコードで実行されるように差し替える)やガベージコレクションなどがあります。

しかし、これらの影響を考慮して、事前にウォームアップ(JITコンパイルが計測対象コードをCPUネイティブコードに置き換えるように)で処理を繰り返し実行し、さらに計測を複数回繰り返して統計処理をするといったことを毎回作って計測するのはかなりの手間です。

OpenJDKプロジェクトにおいてマイクロベンチマークの枠組みが開発されているので、これを利用してベンチマークを実施するのが楽です。

準備

標準的な準備(maven)

2014年時点のものですがブログ(はてな日記)に少し書いたものがあります。
http://d.hatena.ne.jp/torutk/20141013/p1

オフラインで使用するための準備

インターネットから切り離された環境で使用する場合、mavenは極めて不便です。そこで、必要なJARファイルをダウンロードして使用します。

必要なライブラリ

オフラインで実行するのに必要なJARファイル(ライブラリ)は次の3つです。

jmh-coreとjmh-generator-annprocess は、上述URL(mavenリポジトリ)から最新バージョンのディレクトリを辿り、JARファイルをダウンロードします(同ディレクトリにソースファイルを固めたJARとJavadocを固めたJARもあるので必要なら併せてダウンロードします)。

commons-math3は、上述URL(apacheのダウンロードページ)からバイナリのzip(またはtar.gz)をダウンロードし、その中に入っているJARファイルを取り出します。

これらをクラスパス上に配置して使用します。

NetBeansのAntライブラリとして設定する

ライブラリを保存するディレクトリを決めて(例:C:\Program Files\Java\jmh)、その中にダウンロードしたJARファイルを置きます。

C:\Program Files\Java\jmh
  +-- commons-math3-3.6.1.jar
  +-- jmh-core-1.19.jar
  +-- jmh-generator-annprocess-1.19.jar

NetBeansの[ツール]メニュー > [ライブラリ] で、[新規ライブラリ]ボタンを押し、ライブラリ名に JMH と入力、[クラスパス]タブで[JAR/フォルダの追加]ボタンを押し、上述のjarファイル3つを追加します。

あとは、JMHを使用するプロジェクトのプロパティで、ライブラリにJMHライブラリを追加します。

JMHの使い方入門

Hello worldの計測

最小限のベンチマーク記述

最小限の構成でベンチマークプログラムを書きます。

public class Hello {

    @Benchmark
    public String greet() {
        return String.format("%s, %s", "Hello", "world");
    }
}

最低限の記述では、ベンチマークで計測したいメソッドにアノテーション@Benchmark を付けます。
ベンチマーク対象メソッドに引数があると、別なアノテーションが必要になるので、ここでは引数を空にしています。

次は、ベンチマークを走らせる処理を記述します。別なクラスでも、このクラスのmainメソッドでも構いません。

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(Hello.class.getSimpleName())
                .forks(1)
                .build();
        new Runner(opt).run();
    }

includeでは、ベンチマークを実行するメソッドを持つクラス名を指定します。
forksでは、ベンチマークの実行サイクル数(ウォームアップおよび計測を1サイクルとして)を指定します。デフォルトは10です。
その他、ここでは明示的に指定していませんが、ウォームアップと計測の各サイクルでベンチマーク対象メソッドを呼ぶ回数(イテレーション回数)や、計測方法(スループット、平均時間、など)、スレッドの生成などが指定できます。

最小限のベンチマークの実行

実行結果全体を

結論だけ見るならば、メッセージの一番下を見ます。

Benchmark     Mode  Cnt       Score       Error  Units
Hello.greet  thrpt   20  877680.284 ± 45186.390  ops/s

ベンチマーク対象メソッドは、Helloクラスのgreetメソッドで、計測方法はスループットモード、計測回数は20回でその計測値は、877,680 回/秒が平均値、誤差(偏差)がプラスマイナス45,186回/秒となります。この誤差は、結果の直前にある次のメッセージから、信頼水準99.9%の信頼区間の幅であると思われます。

  CI (99.9%): [832493.894, 922866.674] (assumes normal distribution)

初期化・終了メソッド

メソッドに、@Setup、@TearDown アノテーションを指定して、計測前後にメソッドを実行することができます。
イテレーションの度にその前後で実行するか、テストサイクルの前後で1回実行するかを指定します。

  • @Setup(Level.Trial)
    ウオームアップのイテレーション開始前(最初のイテレーション?)に1回実行
  • @Setup(Level.Iteration)
    ウォームアップと計測の各イテレーション開始前に実行
  • @Setup(Level.Invocation)
    各イテレーションの中でベンチマーク対象メソッドを呼ぶ前に実行

状態(インスタンス)の共有範囲(スコープ)

クラスに、@State アノテーションを指定して、そのクラスのインスタンスが共有される範囲を指定します。
ベンチマークでは複数スレッドが使用されます。

  • @State(Scope.Benchmark)
    ベンチマーク全体で(複数スレッドから)共有
  • @State(Scope.Thread)
    ベンチマークを実施する個々のスレッド毎にインスタンス生成
  • @State(Scope.Group)
    ベンチマークを実施するスレッドグループ毎にインスタンス生成

注意点

JITの最適化で処理が実行されない問題に対処

JITは賢いので、計算してもその後で使わずに捨てられてしまうものはデッドコードとして最適化で消されしまうことがあります。

  • 戻り値として計算結果を返却
    JMHは戻り値を受け取り使う振りをするので、計測に不都合なJIT最適化を抑制します
  • メソッドの引数にBlackhole型の変数を指定、計算結果をその変数のconsumeメソッドに渡す
    複数の計算結果があるとき、戻り値では手間なのでこれを使うとよいようです

トラブルシュート

いろいろなエラー

  • Field "randoms" is declared within the class not having @State annotation.
    フィールドを持つ(状態を持つ)場合に、クラスに@Stateアノテーションを指定しないとき

参考文献

英語

日本語

クリップボードから画像を追加 (サイズの上限: 1 GB)