こんにちは、三苫です。
これは TECHSCORE Advent Calendar 2015 の11日目の記事です。
Arrays#stream(boolean[]) が無い!!
ごくまれにたまに、 boolean[] を Stream<Boolean> にしたいことってありますよね?
ところが Arrays#stream() には boolean[] を引数に受け取ってくれるシグニチャがありません。
というわけで、あるときシナジーマーケティングのエンジニア数人でどんな書き方で変換するのがいい感じか案を出して盛り上がったのその顛末を紹介します。
自分たちでかっこ良く実装しよう
案として出てきたのは以下の4案です。
案1「素直に Stream.Builder で作る」
もっとも素直で正攻法です。
Stream の作成時に全要素の boolean → Boolean のボクシングコストをすべて払っているところが少し気になります。
1 2 3 4 5 6 7 |
public static Stream<Boolean> booleanArrayToStream1(boolean[] a) { Stream.Builder<Boolean> builder = Stream.builder(); for (boolean value : a) { builder.add(Boolean.valueOf(value)); } return builder.build(); } |
案2「配列をリストでラップする」
boolean配列を AbstractList でラップして stream() を呼び出して作っちゃう案。
内部クラスなのが気になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static Stream<Boolean> booleanArrayToStream2(boolean[] a) { return new AbstractList<Boolean>() { @Override public Boolean get(int index) { return a[index]; } @Override public int size() { return a.length; } }.stream(); } |
案3「Guavaで一発」
Guava (https://github.com/google/guava)でリスト化してから Stream に変換。
内部処理的には「配列をリストでラップする案」とほぼ同じですが記述が一番少なくわかりやすいです。
が、外部ライブラリに依存しているのが気になります。(いろいろ気にしてばっかりだな)
1 2 3 |
public static Stream<Boolean> booleanArrayToStream3(boolean[] a) { return Booleans.asList(a).stream(); } |
案4「IntStream を map する」
エレガントさを追求した案です。
依存も内部クラスもありませんが、添字のための IntStream が前面に出てきているため初見時の読みやすさに難有りです。
1 2 3 |
public static Stream<Boolean> booleanArrayToStream4(boolean[] a) { return IntStream.range(0, a.length).mapToObj(i -> a[i]); } |
かっこよさはわかった。で、それは使い物になるのかい?
ではこの 4 案、それぞれどのような特性があるでしょうか?
以下のように boolean -> Stream<Boolean> に変換したあと、true の数を数えるベンチマークをとってみましょう。
1 2 3 4 5 6 |
AtomicInteger sum = new AtomicInteger(); booleanArrayToStream(testData).forEach(b -> { if (b) { sum.incrementAndGet(); } }); |
計測には jmh (http://openjdk.java.net/projects/code-tools/jmh/)を用いました。
計測マシンのスペックは以下の通り。
マシン:ThinkPad X200s(CPU:Core2 Duo 2.13GHz, Mem:4GB)
Java:64-Bit Server VM (build 25.66-b17, mixed mode)
計測結果は以下のようになりました。
1,000 要素の boolean 配列の場合
1 2 3 4 5 |
Benchmark Mode Cnt Score Error Units booleanArrayToStream1 thrpt 200 50784.388 ± 489.944 ops/s booleanArrayToStream2 thrpt 200 51205.927 ± 209.787 ops/s booleanArrayToStream3 thrpt 200 22476.731 ± 139.976 ops/s booleanArrayToStream4 thrpt 200 37495.190 ± 196.044 ops/s |
グラフにするとこんな感じ。
要素数が1000と少ない時には「配列をリストでラップする」が最も高速です。
インスタンスの生成コストが他の3案よりも低いのかなという気がしますね。
また「素直に Stream.Builder で作る」も思ったよりも健闘しています。
「Guavaで一発」は内部的に使われている BooleanArrayAsList が今回の要件よりも
高機能なので今回の計測で不利なのはいたしかたなしでしょう。
Score の単位は ops/s なので一秒間に何回処理を実行できたかです。
1,000,000 要素の boolean 配列の場合
1 2 3 4 5 |
Benchmark Mode Cnt Score Error Units booleanArrayToStream1 thrpt 200 47.253 ± 0.497 ops/s booleanArrayToStream2 thrpt 200 50.542 ± 0.402 ops/s booleanArrayToStream3 thrpt 200 23.000 ± 0.137 ops/s booleanArrayToStream4 thrpt 200 55.630 ± 0.166 ops/s |
グラフにするとこんな感じ。
要素数100万とそれなりに数がある時、傾向が変わり「IntStream を map する」が最も高速でした。
次点は要素数1,000の時のトップである「配列をリストでラップする」です。要素数が多い時も少ない時も安定したパフォーマンスを出していると言えそうです。
計測したあとの所感
単純な変換ですがそれでもいろいろなやり方があり、それぞれ特性がありパフォーマンスにも明らかな差が出ることがわかりました。
マイクロベンチマークは多くの場合実際に構築するシステムに影響ない場合が多いのでそこまでこだわる必要がないですが、それでも折りにふれてやってみると新たな発見があります。
また、こういう単純な処理のコスト感もわかるので「あ、ここちょっとパフォーマンス改善の余地がある書き方だけど利得はせいぜい10ms。なら、そこまでカリカリに書かなくてもいいよね」みたいな簡単な判断もしやすくなります。(とはいえ、パフォーマンス改善の基本は目算ではなくまずは計測ですからね!)
ちなみに私のイチオシは強く Java8 感を味わえる「IntStream を map する」です。
それでは!