連載
» 2014年04月30日 18時00分 公開

ラムダ式で本領を発揮する関数型インターフェースとStream APIの基礎知識Java 8はラムダ式でここまで変わる(3)(3/3 ページ)

[長谷川智之,株式会社ビーブレイクシステムズ]
前のページへ 1|2|3       

Stream APIの並列処理はマルチコアの恩恵を受けて高速化できるのか

 Stream APIの特徴の1つに並列処理の実装を容易にできることがあります。現在、多くのコンピューターのCPUは複数のコアを持っています。そして、それらの各コアが処理を同時に行うことで負荷を分散し処理速度を上げています。

 しかし、単純なforループのように処理を1つのスレッドで順々に行う直列処理の場合、それらの恩恵を受けることができませんでした。一方で、並列処理を使えるようになれば、複数コアのCPUの利点を生かしたパフォーマンスの改善が期待できるようになります。

streamメソッドをparallelStreamメソッドに書き換えるだけ

 では、実際に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回実行してみました。

  1. forループで行った場合
  2. streamメソッドで行った場合
  3. parallelStreamメソッドで行った場合

 実装は次のようになります。

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メソッドの場合よりも速く処理が行われていることが分かります。

【注意点1】対象となる要素が少ない場合は、並列処理の方が遅くなる

 しかし、並列処理にしたからといって全てのケースでパフォーマンス改善が行われるわけではありません。並列処理を行う場合、内部の処理ですべきことが多くなるため直列処理よりも大きなオーバーヘッドが発生することになります。

 対象となる要素が多い場合、そのオーバーヘッドのデメリットよりも並列で処理をすることによる高速化の方がメリットは大きくなりますが、要素が少ない場合、逆にオーバーヘッドのデメリットの方が大きくなってしまい、並列処理の方が遅くなってしまうこともあります。

 例えば、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

 そのため、やみくもに並列処理にすることでパフォーマンスの改善が図れるわけではないので、注意してください。

【注意点2】コア数によってパフォーマンスは変わる

 また、コア数によって処理のパフォーマンスは変わってきます。4コアのCPUのマシンで良いパフォーマンス結果を得たからといって、2コアのCPUのマシンにそれを移植したとしても、同じように良いパフォーマンス結果を得られるというわけではありません。

【注意点3】元データの並び順は保障されない

 また、並列処理を使った際の注意事項として「元となるデータが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
実行結果

次回はStream APIの主なメソッド

 今回はJava 8で追加されたjava.util.functionパッケージの汎用的な関数型インターフェースとJava 8の特に注目されているStream APIについて見てきました。

 このStream APIの概要が分かったところで、次回は実際Stream APIはどのように使われるのか主なメソッドについて見ていきます。そして、そこでラムダ式がどのように使われているのかを解説します。ご期待ください。

著者プロフィール

長谷川 智之(はせがわ ともゆき)

株式会社ビーブレイクシステムズ開発部所属。

社内サークル執筆チーム在籍。

主な執筆。

@IT連載『Javaの常識を変えるPlay framework入門

日経ソフトウェア連載『コツコツ学ぶAndroidネイティブアプリ開発教室』


前のページへ 1|2|3       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。