Stream APIの特徴の1つに並列処理の実装を容易にできることがあります。現在、多くのコンピューターのCPUは複数のコアを持っています。そして、それらの各コアが処理を同時に行うことで負荷を分散し処理速度を上げています。
しかし、単純なforループのように処理を1つのスレッドで順々に行う直列処理の場合、それらの恩恵を受けることができませんでした。一方で、並列処理を使えるようになれば、複数コアのCPUの利点を生かしたパフォーマンスの改善が期待できるようになります。
では、実際にStream APIを使って並列処理の実装を行ってみましょう。先ほどのStream APIサンプルでは並列処理ではなく直列処理を行っています。並列処理にするには、このCollectionのstreamメソッドをparallelStreamメソッドに書き換えるだけで並列処理に変更できます。
今回のサンプルを並列処理に変えると、次のようになります。
int maxSalesAmount = salesDataList.parallelStream() ←streamをparallelStreamに変更 .filter(salesData -> salesData.getLocation().equals("東京")) .mapToInt(salesData -> salesData.getSalesAmount()) .max() .getAsInt();
マルチコアのCPUのマシンで並列処理を行うと本当に効果があるのか実際に試してみましょう。今回は上記の処理に2014年1月以前という条件を加えて、次の3パターンで5回実行してみました。
実装は次のようになります。
public static void main(String[] args) { // 2014年1月末日を取得 Calendar calendar = Calendar.getInstance(); calendar.clear(); calendar.set(2014, 1, 31); Date endDateOfJanuary = calendar.getTime(); // テストデータの生成 List<SalesData> salesDataList = SalesData.createSalesDateList(); // 5回実行 for (int i = 1; i <= 5; i++) { System.out.println(i + "回目 ------------------------------"); // forループの場合 int maxSalesAmount = -1; long start = System.currentTimeMillis(); for (SalesData salesData : salesDataList) { if (salesData.getLocation().equals("東京")) { if (salesData.getDate().compareTo(endDateOfJanuary) <= 0) { if (maxSalesAmount < 0) { maxSalesAmount = salesData.getSalesAmount(); } else if (maxSalesAmount < salesData.getSalesAmount()) { maxSalesAmount = salesData.getSalesAmount(); } } } } long end = System.currentTimeMillis(); System.out.println("forループ: time=" + (end - start) + "ms"); // streamメソッドの場合 maxSalesAmount = -1; start = System.currentTimeMillis(); maxSalesAmount = salesDataList.stream() .filter(salesData -> salesData.getLocation().equals("東京")) .filter(salesData -> salesData.getDate().compareTo(endDateOfJanuary) <= 0) .mapToInt(salesData -> salesData.getSalesAmount()) .max() .getAsInt(); end = System.currentTimeMillis(); System.out.println("stream: time=" + (end - start) + "ms"); // parallelStreamメソッドの場合 maxSalesAmount = -1; start = System.currentTimeMillis(); maxSalesAmount = salesDataList.parallelStream() .filter(salesData -> salesData.getLocation().equals("東京")) .filter(salesData -> salesData.getDate().compareTo(endDateOfJanuary) <= 0) .mapToInt(salesData -> salesData.getSalesAmount()) .max() .getAsInt(); end = System.currentTimeMillis(); System.out.println("parallelStream: time=" + (end - start) + "ms"); } }
これを4コアのCPUのマシンで実行したところ、次の結果になりました。
1回目 ------------------------------ forループ: time=282ms stream: time=420ms parallelStream: time=322ms 2回目 ------------------------------ forループ: time=536ms stream: time=563ms parallelStream: time=162ms 3回目 ------------------------------ forループ: time=483ms stream: time=683ms parallelStream: time=154ms 4回目 ------------------------------ forループ: time=485ms stream: time=689ms parallelStream: time=150ms 5回目 ------------------------------ forループ: time=374ms stream: time=559ms parallelStream: time=149ms
この結果を見るとStream APIの並列処理(parallelStream)を使った場合、最初の1回目は時間を取られていますが、それ以降は直列処理のforループやstreamメソッドの場合よりも速く処理が行われていることが分かります。
しかし、並列処理にしたからといって全てのケースでパフォーマンス改善が行われるわけではありません。並列処理を行う場合、内部の処理ですべきことが多くなるため直列処理よりも大きなオーバーヘッドが発生することになります。
対象となる要素が多い場合、そのオーバーヘッドのデメリットよりも並列で処理をすることによる高速化の方がメリットは大きくなりますが、要素が少ない場合、逆にオーバーヘッドのデメリットの方が大きくなってしまい、並列処理の方が遅くなってしまうこともあります。
例えば、SalesDataのListを生成するのに日数のループを100万日から10日に減らした場合、結果は次のようになり、並列処理の方が遅くなっています(実行時間がかなり短くなるため、結果をナノ秒で算出するように変更しています)。
1回目 ------------------------------ forループ: time=408000ns stream: time=36956000ns parallelStream: time=7851000ns 2回目 ------------------------------ forループ: time=87000ns stream: time=165000ns parallelStream: time=193000ns 3回目 ------------------------------ forループ: time=78000ns stream: time=115000ns parallelStream: time=246000ns 4回目 ------------------------------ forループ: time=126000ns stream: time=173000ns parallelStream: time=216000ns 5回目 ------------------------------ forループ: time=125000ns stream: time=158000ns parallelStream: time=218000ns
そのため、やみくもに並列処理にすることでパフォーマンスの改善が図れるわけではないので、注意してください。
また、コア数によって処理のパフォーマンスは変わってきます。4コアのCPUのマシンで良いパフォーマンス結果を得たからといって、2コアのCPUのマシンにそれを移植したとしても、同じように良いパフォーマンス結果を得られるというわけではありません。
また、並列処理を使った際の注意事項として「元となるデータがListのように並び順が保障されている集合体でも、並列処理の際はその順番通り実行されるわけではない」点があります。
例えば、次のようにforEachメソッドを使ってListの要素を標準出力する場合、直列処理だとListの順番通り出力されますが、並列処理だとListの順番通り出力されるわけではありません。
List<String> list = Arrays.asList( "list1", "list2", "list3", "list4", "list5" ); list.stream().forEach(e -> System.out.println("stream: " + e)); list.parallelStream().forEach(e -> System.out.println("parallelStream: " + e));
stream: list1 stream: list2 stream: list3 stream: list4 stream: list5 parallelStream: list3 parallelStream: list5 parallelStream: list4 parallelStream: list2 parallelStream: list1
今回はJava 8で追加されたjava.util.functionパッケージの汎用的な関数型インターフェースとJava 8の特に注目されているStream APIについて見てきました。
このStream APIの概要が分かったところで、次回は実際Stream APIはどのように使われるのか主なメソッドについて見ていきます。そして、そこでラムダ式がどのように使われているのかを解説します。ご期待ください。
長谷川 智之(はせがわ ともゆき)
株式会社ビーブレイクシステムズ開発部所属。
社内サークル執筆チーム在籍。
主な執筆。
@IT連載『Javaの常識を変えるPlay framework入門』
日経ソフトウェア連載『コツコツ学ぶAndroidネイティブアプリ開発教室』
Copyright © ITmedia, Inc. All Rights Reserved.