本ブログの読者の皆様方におかれましては、JavaのArrayListとLinkedListの
実装の違いにより性能に差があることは、当然のように熟知のことと存じあげます。
しかし!
実際にいかほどの差があるのか、それを数値で説明できるという方はどれほどいらっしゃるでしょうか。
いきなり丁寧語の煽りでスタートしました @cero_t です。
そう、今日のテーマはマイクロベンチマークです。
たとえば、
など、よくあるパフォーマンスのプラクティスについては既に知っているという方も多いと思うのですが
実際に何倍ぐらいの差なのか(どれぐらいのオーダーの差なのか)を数値で話すことができるという方は、
あまり多くないように思います。というか私もできません。
そこで登場するのがベンチマークなわけです。
ただ、このような小さな処理のベンチマークというものは一般的に計測が難しく、
特にJavaの場合、JITコンパイルやGCの影響があるため、
安定したベンチマークを提示することが難しいと言われています。
そこで登場するのが、OpenJDKのサイトで公開されている「JMH」というツールです。
1. JMHとは
JMHは単位時間あたり(1秒、1ミリ秒、1マイクロ秒・・・)の処理回数を計測することに特化したツールです。
http://openjdk.java.net/projects/code-tools/jmh/
JMHが何の略かはサイトには掲載されていませんが
Java harnessという記載があるので、Java Micro-benchmarking Harnessか何かだと推測します。
JMHはマイクロベンチマークに必要なこと、たとえば・・・
- ベンチマーク前のウォームアップ
- 複数回の計測実施
- 平均や平均誤差などの統計分析
などを行なってくれるほか、
ベンチマークの実行回数や時間などを簡単なパラメータ指定で変更できます。
2. JMHのビルド
残念なことにJMHはダウンロード可能なJARファイルなどは提供されておらず、自分でビルドする必要があります。
ビルドにはMavenが必要なので、事前にMavenのセットアップを済ませておいてください。
2-1. ソースコードのダウンロード
まず、OpenJDKのサイトよりJMHのソースコードをダウンロードします。
Mercurial (HG) をセットアップ済みの方は、公式サイトにもあるようにhgコマンドでチェックアウトができます。
hg clone http://hg.openjdk.java.net/code-tools/jmh/ jmh
また、Mercurialをセットアップせず、ブラウザで直接ダウンロードすることも可能です。
以下のリポジトリにブラウザでアクセスして、上部にある「zip」というリンクからダウンロードできます。
http://hg.openjdk.java.net/code-tools/jmh/
念のため、直リンクのURLも掲載しておきましょう。
http://hg.openjdk.java.net/code-tools/jmh/archive/tip.zip
これでソースコードの入手は完了です。
3. ベンチマークの作成
ビルドが完了したら、いよいよベンチマークの作成に移ります。
3-1. ベンチマーク用プロジェクトの作成
公式サイトにはmvnコマンドを使う方法と、pom.xmlに直接dependencyを指定する方法の
両方が提示されていますが、もちろんどちらでも構いません。
私の場合はIntelliJでMaven Projectを作成してからpom.xmlにdependencyを記述しました。
<dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
3-2. ベンチマークの作成
さて、ようやくベンチマーク本体の作成を始められるようになりました。
今回はArrayListの初期サイズを指定した場合と、指定しなかった場合でどれぐらいの差になるのかを計測してみましょう。
package jp.co.acroquest.benchmark; import org.openjdk.jmh.Main; import org.openjdk.jmh.annotations.GenerateMicroBenchmark; import java.util.ArrayList; import java.util.List; public class ArrayListBenchmark { private static final int size = 100; // ★1 ベンチマークメソッドにアノテーションをつける @GenerateMicroBenchmark public void withInitialSize() { List<Integer> sizedList = new ArrayList<>(size); for (int i = 0; i < size; i++) { sizedList.add(i); } } @GenerateMicroBenchmark public void withoutInitialSize() { List<Integer> defaultList = new ArrayList<>(); for (int i = 0; i < size; i++) { defaultList.add(i); } } // ★2 Mainメソッドを直接呼び出して、ベンチマークを実行する public static void main(String[] args) { Main.main("-i 3 -wi 3 -f 1".split(" ")); } }
簡単にソースコードのポイントを紹介します。
★1 ベンチマークメソッドにアノテーションをつける
ベンチマークを行なうメソッドには@GenerateMicroBenchmarkアノテーションをつけます。
ちょうどJUni4で言うところの@Testアノテーションのようなものです。
★2 Mainメソッドを直接呼び出して、ベンチマークを実行する
公式サイトにはビルドしてjarを生成してからベンチマークを行なう方法が記載されていますが
もちろんJMHのmainメソッドを直接呼び出して、ベンチマークを実行することもできます。
ちなみにこのソースコードの例では「ベンチマークのメソッド呼び出しを1秒間繰り返す」という処理を、
ウォームアップのために3回(-wi 3)、計測のために3回(-i 3)全体を1セット(-f 1)行なうような
オプションを指定して、mainメソッドを呼んでいます。
3-3. ベンチマークの実行(動作確認)
このソースコードをビルドして実行をすれば、ベンチマークを実施できます。
私の環境(MacBook Air Mid2011)では、このような結果が得られました。
Benchmark Mode Thr Cnt Sec Mean Mean error Units j.c.a.b.ArrayListBenchmark.withInitialSize thrpt 1 3 1 1895.013 220.400 ops/ms j.c.a.b.ArrayListBenchmark.withoutInitialSize thrpt 1 3 1 1220.345 110.302 ops/ms
初期サイズを指定した場合は1895回/ms、初期サイズを指定しない場合は1220回/msでした。
4. ベンチマークの実行
上で実行した方法は、あくまでもIDE上で実行する簡易的な動作確認のようなものでした。
より安定したベンチマークを行なうためには、IDE上で動作させるのではなく
ビルドしてJARファイルを生成し、java -jarコマンドで実行した方が良いでしょう。
4-1. ベンチマークのビルド&実行
ビルド時にJARファイルを作成するために、maven-shade-pluginの設定をpom.xmlに追加します。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <finalName>microbenchmarks</finalName> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>org.openjdk.jmh.Main</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>
これでビルド時にmicrobenchmarks.jarが生成されます。
実際にビルドを行なってみましょう。
mvn package
これでtargetディレクトリにmicrobenchmarks.jarが生成されるはずです。
4-2. ベンチマークの実行
ビルドしたJARファイルを用いて、ベンチマークを実行します。
java -jar target/microbenchmarks.jar -wi 5 -i 5 -f 2
オプションについては後で改めて紹介しますが、
この例ではウォームアップを5回、計測を5回、全体で2セットのベンチマークを実施しています。
実行した結果、このような統計が得られました。
Benchmark Mode Thr Cnt Sec Mean Mean error Units j.c.a.b.ArrayListBenchmark.withInitialSize thrpt 1 10 1 1781.790 34.361 ops/ms j.c.a.b.ArrayListBenchmark.withoutInitialSize thrpt 1 10 1 1129.782 14.142 ops/ms
なるほど、初期サイズを指定した方が1.5〜1.6倍ぐらいは高速なわけですね。
初期サイズを指定しなければ、配列のサイズを増やす際に
配列の作成とコピーが行なわれるわけですが、それがきちんと数字に表れました。
ちなみに要素数を100から100万に増やして同じ試験を実施してみたところ
スループットの差は1.3倍ぐらいまで縮まりました。
要素数が増えれば増えるほど、処理全体に占める配列の作成・コピーの割合が少なくなるため
スループットの差が小さくなるわけですね。そのようなことも簡単に計測ができます。
5. コマンド引数について
最後に、コマンド引数について紹介しましょう。
JMHでは、コマンドに引数を渡すことで、ウォームアップの回数や実行回数、実行時間などを変更することができます。
代表的なオプションについて、以下に表でまとめておきます。
オプション | 概要 | 選択肢 | 初期値 |
---|---|---|---|
-i | 計測を繰り返す回数 | (数値) | 20 |
-wi | ウォームアップ繰り返す回数 | (数値) | 20 |
-tu | 結果を表記する際の単位 | m, s, ms, us, ns | ms |
-f | フォークの数。今回の例では計測全体の繰り返し数 | (数値) | 10 |
-r | ベンチマークの実行時間 | (時間: 10s, 200msなど) | 1s |
ひとまずはこの辺りを把握しておけば、問題なくベンチマークが行なえると思います。
6. まとめ&おまけ
「JMHはマイクロベンチマークを行なうための良いツールだ!」
簡単ですが、まとめは以上です。 #簡単すぎ
また、今回、私が作成したベンチマークやpom.xmlをgithubに置いておきました。
事前に「JMHのビルド」まで済ませたうえで、動かしてみてください。
https://github.com/cero-t/Benchmarks
今回は簡単な例のみ紹介しましたが、JMHは複数のJVMを利用したベンチマークや
マルチスレッド環境でのベンチマークなども行なうことができる、優秀なツールです。
もし今回のエントリーに反響があれば、より詳しい記事を書こうと思いますので
続きを読みたいという方は、ぜひツイートや、はてブなどでお知らせください。
それでは、レッツエンジョイ、マイクロベンチマーク!
Acroquest Technologyでは、キャリア採用を行っています。
- 日頃勉強している成果を、Hadoop、Storm、NoSQL、HTML5/CSS3/JavaScriptといった最新の技術を使ったプロジェクトで発揮したい。
- 社会貢献性の高いプロジェクトに提案からリリースまで携わりたい。
- 書籍・雑誌等の執筆や対外的な勉強会の開催を通した技術の発信や、社内勉強会での技術情報共有により、技術的に成長したい。
- OSSの開発に携わりたい。
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
キャリア採用ページ
11/24(日) DODA転職フェアに参加します!ぜひブースにお立ち寄りください。