Javaのパフォーマンス検証
前書き
コードっていろんな書き方出来ますよね その時可読性を意識するか、パフォーマンスを意識するか、とりあえずモダンな機能で書いてみるかとかー..etc 色々選択肢があるわけで
個人的には 可読性 > モダンな書き方 > パフォーマンスくらいの優先度で書いてます (正直 C/C++とかみたいな世界でもない限り, よっぽどパフォーマスん酷くなければ無視していいだろと思ってます..)
でもパフォーマンス気にしないといっても、最近は気になるわけで ググったりするけど、
◯◯ 処理はさほどパフォーマンス落ちないので、ぜひこちらを使いましょう
みたいな記事が多いので、実感が持ちづらい やっぱり自分で手を動かして、実際に計測値を見て判定したい(Golang の testing.b 的な)
ということで Java でメソッドとか機能とかの小さい単位でベンチマークできるJMHを調べて見ました。 (マイクロベンチマークっていうらしいですね)
JMH?
JDK 公式のマイクロベンチマークツールとのこと
サンプルプロジェクト立てる
依存ライブラリとかビルド用に gradle プロジェクトを立てます
Intellij で Project -> new -> Gradle でやりましょう
適当な groupId と artifactId つけて

JMH のビルド
Gradle 用のプラグインを使います https://github.com/melix/jmh-gradle-plugin
build.gradle を修正
apply plugin: "java"
group 'microbenchmark-sample'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
// gradleプロジェクトでjmhを使えるようにする設定
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.7"
}
}
apply plugin: "me.champeau.gradle.jmh"
// ライブラリのダウンロード先指定
repositories {
mavenCentral()
}
// jmhの依存関係解決
dependencies {
compile 'org.openjdk.jmh:jmh-core:1.21'
}
// jmhの設定
jmh {
jmhVersion = '1.21'
warmupIterations = 5
iterations = 20
fork = 2
benchmarkMode = ['thrpt']
failOnError = true
}
jmhJar {
destinationDir = projectDir
}
ベンチマーク対象クラスを作成
今回は下のような機能でいくつかの書き方を比較してみる
- DB へアクセスしてユーザ名リストを取得し, 先頭 1 件のユーザー名を大文字にして返却する
- DB から取得する時点で期待したソートは実施されているとする
- 1 件もヒットしない場合のことも考慮すること
フォルダを掘る
~/w/bcmk-sample> mkdir -p src/main/java/bcmk
~/w/bcmk-sample> touch src/main/java/bcmk/App.java
3 つのメソッドを用意
package bcmk;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
public class App {
// 乱数生成用
private final static Random random = new Random();
/**
* if文のやつ
* @return
*/
public Optional<String> getUserName_legacy() {
List<String> userNameList = getUserNameList();
if(userNameList.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(userNameList.get(0).toUpperCase());
}
}
/**
* 三項演算子
* @return
*/
public Optional<String> getUserName_old() {
List<String> userNameList = getUserNameList();
return userNameList.isEmpty()
? Optional.empty()
: Optional.of(userNameList.get(0).toUpperCase());
}
/**
* Stream使う
* @return
*/
public Optional<String> getUserName_stream() {
List<String> userNameList = getUserNameList();
return userNameList.stream().findFirst().map(s -> s.toUpperCase());
}
/**
* 空リストか要素入りのリストが返される..DBからの取得の代用(計測の本質は DBじゃないため
* @return
*/
private List<String> getUserNameList() {
List<String> userNameList = new ArrayList<>();
userNameList.add("taro");
userNameList.add("moge");
return random.nextBoolean()
? new ArrayList<String>()
: userNameList;
}
}
3 つとも同じ機能です。 空か要素入りの List が返ってくるメソッドを呼び出して 結果が0件の場合は Optional.empty()を空でなければ、1 件目を取り出して大文字にして返します。
※Optional は Java で null 安全にコード書くためのやつです、ぬるぽに悩まされないしコードの美しさが上がるります → https://qiita.com/shindooo/items/815d651a72f568112910
1 つ目の if 文利用は見た目が汚いので個人的に嫌です。(return 二つが気持ち悪い
2 つ目の三項演算子利用は妥協です。(コードに 0 とかの数値とかが直で描かれるのが美しさを感じません
3 つ目の Stream がいいですよね.
これはメソッドから受け取った List を一旦 Stream にして、findFirst メソッドを呼びます.
返却の型は Optional
個人的には Stream 利用推しということです。
ベンチマーク用のパッケージを作成
上で書いた 3 つのメソッドを比較して行きましょう src/jmh というフォルダを掘ります
~/w/bcmk-sample> mkdir -p src/jmh/java/bcmk
~/w/bcmk-sample> touch src/jmh/java/bcmk/SampleBenchmark.java
で SampleBenchmark.java には下のようなコードです
package bcmk;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class SampleBenchmark {
@Benchmark
public void if文の古いコード() {
final bcmk.App m = new bcmk.App();
System.out.print("Result: " + m.getUserName_legacy().orElse("該当ユーザーはいませんでした"));
}
@Benchmark
public void 三項演算子() {
final bcmk.App m = new bcmk.App();
System.out.print("Result: " + m.getUserName_old().orElse("該当ユーザーはいませんでした"));
}
@Benchmark
public void Stream使ったやつ() {
final bcmk.App m = new bcmk.App();
System.out.print("Result: " + m.getUserName_stream().orElse("該当ユーザーはいませんでした"));
}
}
先ほど作ったクラスを読み込んで、3つのメソッドをそれぞれ実行します あとはアノテーションをつければよしなに計測してくれます デフォだとスループット(秒間の処理実行回数)を計測します
実行
Intellij だったら GUI から実行か

もしくはターミナルで下のコマンド実行でいけます
~/w/bcmk-sample> ./gradlew clean jmh
2、30 分かかります
結果
Result "bcmk.SampleBenchmark.三項演算子":
134.958 ±(99.9%) 10.147 ops/ms [Average]
(min, avg, max) = (94.187, 134.958, 167.342), stdev = 18.036
CI (99.9%): [124.811, 145.104] (assumes normal distribution)
# Run complete. Total time: 00:25:04
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
SampleBenchmark.Stream使ったやつ thrpt 40 154.498 ± 9.895 ops/ms
SampleBenchmark.if文の古いコード thrpt 40 146.304 ± 7.848 ops/ms
SampleBenchmark.三項演算子 thrpt 40 134.958 ± 10.147 ops/ms
BUILD SUCCESSFUL in 25m 12s
6 actionable tasks: 1 executed, 5 up-to-date
18:03:17: Task execution finished 'jmh'.
Score が秒間の実行回数(つまり多ければ多いほどいいです) Error は誤差ですね Score の ±Error 値が信頼値です
Stream が一番実行できてますね! パフォーマンスでも問題なさそうです (参考演算子がいちばんよくないんですね…参考演算子すきなんですが…
一応平均処理時間も見ておきましょう
計測モードの変更
build.grade の設定を変えます
// jmhの設定
- jmh {
- jmhVersion = '1.21'
- warmupIterations = 5
- iterations = 20
- fork = 2
- benchmarkMode = ['thrpt']
- failOnError = true
- }
// モードを消します(コードで設定します)
// 処理時間かかるので, iterationsとかも落とします
+ jmh {
+ jmhVersion = '1.21'
+ warmupIterations = 2
+ iterations = 5
+ fork = 2
+ failOnError = true
+ }
@Benchmark
+ @BenchmarkMode(Mode.AverageTime)
public void Stream使ったやつ() {
final bcmk.App m = new bcmk.App();
System.out.print("Result: " + m.getUserName_stream().orElse("該当ユーザーはいませんでした"));
}
各メソッドに BenchmarkMode アノテーションを付与します
それで再実行
結果
Result "bcmk.SampleBenchmark.三項演算子":
0.008 ±(99.9%) 0.002 ms/op [Average]
(min, avg, max) = (0.007, 0.008, 0.011), stdev = 0.001
CI (99.9%): [0.006, 0.010] (assumes normal distribution)
# Run complete. Total time: 00:07:04
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
SampleBenchmark.Stream使ったやつ avgt 10 0.007 ± 0.001 ms/op
SampleBenchmark.if文の古いコード avgt 10 0.006 ± 0.001 ms/op
SampleBenchmark.三項演算子 avgt 10 0.008 ± 0.002 ms/op
BUILD SUCCESSFUL in 7m 6s
6 actionable tasks: 1 executed, 5 up-to-date
18:15:01: Task execution finished 'jmh'.
同じくらいですね。 問題ないと思います
終わり
今後はこれでちょくちょく気になったら見て行こうかなと思う