仕事で下記を書くにあたって、Javaでパフォーマンス計測したのでQiitaに書いてしまおうと思います。
Javaコーディング規約 パフォーマンスについての章があります。パフォーマンス計測はjre1.8.0_74
で行いました。
結果はすべて数回行った平均で記載しています。
ループとStream API
Java8のStream APIいいですよね。僕は好きです。もはやループ書いたら負けだと思う病気にかかってます。
ただ最近、Streamばっかり書いてると、これパフォーマンス悪いんじゃないか?という強迫観念に駆られます。
なので計測してみようと思いました。
計測コード
-
拡張for文
List<String> list = //数値文字列のList List<String> resultList = new ArrayList<>(); for (String string : list) { if (string.endsWith("0")) { resultList.add(string); } } return resultList;
-
Stream API
List<String> list = //数値文字列のList List<String> resultList = list.stream() .filter(s -> s.endsWith("0")) .collect(Collectors.toList()); return resultList;
計測結果
処理するListの件数 | 拡張for文 (ms) | StreamAPI (ms) |
---|---|---|
100万件 | 7 | 9 |
1,000万件 | 88 | 114 |
1億件 | 949 | 1,026 |
2億件 | 1,822 | 2,081 |
遅くはなるけど、ほとんどの場合気にしなくていいですね。
これで良く眠れます。
ラムダ式と匿名クラス
Java8のラムダ式(とかメソッド参照)いいですよね。Streamばかり書いてるのもあるけど、それ以外の場所でも多用してます。
共通関数の設計の幅が広がった気がします。
当然、匿名クラスより効率がいいはずですけど、一応計測しました。
計測コード
-
匿名クラス
Comparator<String> c = new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } };
-
ラムダ式
Comparator<String> c = (o1, o2) -> o1.compareToIgnoreCase(o2);
-
メソッド参照
Comparator<String> c = String::compareToIgnoreCase;
計測結果
処理件数 | 匿名クラス (ms) | ラムダ式 (ms) | メソッド参照 (ms) |
---|---|---|---|
10億回 | 380 | 0(計測不能) | 0(計測不能) |
100億回 | 6,374 | 0(計測不能) | 0(計測不能) |
1京回 | (30秒以上) | 14 | 10 |
わざとパラメータなしでやってます。
ラムダ式・メソッド参照はインスタンス1つしか作らないから当然こうなりますね(参考)
パラメータ渡した場合のラムダ式を次に計測します。
計測コード
-
匿名クラス
new Comparator<String>() { @Override public int compare(String o1, String o2) { return arg.equals("DESC") ? o2.compareToIgnoreCase(o1) : o1.compareToIgnoreCase(o2); } }
-
ラムダ式
Comparator<String> c = (o1, o2) -> arg.equals("DESC") ? o2.compareToIgnoreCase(o1) : o1.compareToIgnoreCase(o2);
計測結果
処理件数 | 匿名クラス (ms) | ラムダ式 (ms) |
---|---|---|
10億回(パラメータあり) | 571 | 572 |
100億回(パラメータあり) | 9,900 | 9,864 |
大体同じインスタンスの扱いになるので当然ですね。
StringBuilder
とStringBuffer
StringBuffer
嫌いですか?僕は嫌いです。古いコードで残ってると少しだけイライラします。
ただ僕のイライラを解消するためだけに動いてるコードに手を入れるという勇気は僕にはないので我慢します。
しかし、気になるパフォーマンス。どうなんでしょうね。
ついでなので+
演算子、String.join
も計測します。
計測コード
-
+
演算子String s = ""; for (int i = 0; i < list.size(); i++) { String string = list.get(i); if (i > 0) { s += ","; } s += string; } return s;
-
StringBuilder
StringBuilder sb = new StringBuilder(); for (int i = 0; i < list.size(); i++) { String string = list.get(i); if (i > 0) { sb.append(","); } sb.append(string); } return sb.toString();
-
StringBuffer
StringBuffer sb = new StringBuffer(); for (int i = 0; i < list.size(); i++) { String string = list.get(i); if (i > 0) { sb.append(","); } sb.append(string); } return sb.toString();
-
String#join
return String.join(",", list);
計測結果
処理するListの件数 |
+ 演算子 (ms) |
StringBuilder (ms) | StringBuffer (ms) | String#join (ms) |
---|---|---|---|---|
1,000件 | 5 | 0(計測不能) | 0(計測不能) | 0(計測不能) |
1万件 | 1,016 | 1 | 1 | 1 |
10万件 | (30秒以上) | 2 | 5 | 5 |
100万件 | (30秒以上) | 29 | 42 | 51 |
この次500万でもやってみたのですがメモリあふれしたのでやめました。
StringBuffer
遅いけど、絶対ダメっというほどではなかったですね。というかString#join
遅い???
ArrayList
とLinkedList
インデックスでループ
LinkedList
ほとんど使わないですけど、いろいろパフォーマンス計測始めたら、ついでに気になりはじめました
パフォーマンス界隈でよく言われてるし。
なのでやってみましたLinkedList
のインデックスループ。
計測コード
以下のコードをArrayList
とLinkedList
それぞれ計測します。
-
for文(List#get(int)によるループ)
int size = list.size(); for (int i = 0; i < size; i++) { String s = list.get(i); //処理 }
-
拡張for文
for (String s : list) { //処理 }
-
forEach
list.forEach(this::処理);
計測結果
処理するListの件数 |
ArrayList for文(List#get(int)によるループ) (ms) |
LinkedList for文(List#get(int)によるループ) (ms) |
ArrayList 拡張for文 (ms) |
LinkedList 拡張for文 (ms) |
ArrayList forEach (ms) |
LinkedList forEach (ms) |
---|---|---|---|---|---|---|
1万件 | 0(計測不能) | 73 | 0(計測不能) | 0(計測不能) | 0(計測不能) | 0(計測不能) |
10万件 | 0(計測不能) | 7,576 | 0(計測不能) | 0(計測不能) | 1 | 2 |
20万件 | 0(計測不能) | 17,740 | 0(計測不能) | 0(計測不能) | 0(計測不能) | 0(計測不能) |
50万件 | 0(計測不能) | (30秒以上) | 0(計測不能) | 2 | 0(計測不能) | 2 |
100万件 | 1 | (30秒以上) | 0(計測不能) | 4 | 0(計測不能) | 4 |
1,000万件 | 16 | (30秒以上) | 8 | 45 | 6 | 44 |
基本的に拡張for文か、List#forEach
使うべきだってことが再認識できました。
LinkedList
のループ処理でindexが欲しいときは、
拡張for文の外でindex変数作って、ループ処理でインクリメントしたほうが良いみたいですね。
(それ以前にLinkedList
ほとんど使わないけど。)
噂によると、拡張for文とList#forEach
だったらList#forEach
がパフォーマンスいいって聞いたことがありますが、
今回の結果では量が少なすぎてわからないですね。
オートボクシング・アンボクシング
※以下の計測は、通常の利用を考えると、正しい情報ではないため参考にしないでください。
kmizuさん コメントでのご指摘ありがとうございます。
FindbugsのBX_BOXING_IMMEDIATELY_UNBOXEDとかBX_BOXING_IMMEDIATELY_UNBOXED_TO_PERFORM_COERCIONとかで検出されるのは直すとして、
ロジック的にオートボクシング・アンボクシングを繰り返さざるを得ないものってどうなんですかね?
そんなにパフォーマンスに影響しますかね?
(たとえばMapのvalueでcount保持したらダメですか?)
計測コード
-
オートボクシング・アンボクシング~~```java
int value1 = 1;
Integer value2 = value1;
int value3 = value2; -
ただの代入~~```java
int value1 = 1;
int value2 = value1;
int value3 = value2;
計測結果
単位はマイクロ秒
| 処理回数 | オートボクシング・アンボクシング (マイクロ秒) | ただの代入 (マイクロ秒) |
|------------------:|------------------:|------------------:|
| 2,000兆回 | 10 | 4 |
一応差は出ましたが、2000兆も処理してこの程度なら、通常は気にする必要なさそうですね。
BigDecimalのZEROとの比較
昔記事書いたのですがBigDecimalの正・負・ZEROの判定はcompareTo
を使うよりもBigDecimal#signum
を使うほうが効率的です。
ただどちらも同じことができますがパフォーマンスはどうでしょうか?
計測コード
-
compareTo利用
BigDecimal value = new BigDecimal("0.0"); if (value.compareTo(BigDecimal.ZERO) == 0) {
-
signum利用
BigDecimal value = new BigDecimal("0.0"); if (value.signum() == 0) {
計測結果
単位はマイクロ秒
処理回数 | compareTo利用 (マイクロ秒) | signum利用 (マイクロ秒) |
---|---|---|
1京回 | 527 | 424 |
ほとんど差がないですねorz
昔、1.7か1.6(憶えてない)測ったときはもっと差があったはずなのですが、
Java8でcompareToが改善されたってことですかね?
今後、仕事でパフォーマンス計測するようなことがあれば、たぶんここに追記していきます。
GitHubに上げてますのでそちらも興味あればぜひ見てみてください。