Java Stream APIに対する様々な考察

15922 ワード

Streamはjava 8から出現した文法で、以前使用されていたfor loopよりも毒性が優れているため広く使われている.しかしfor−loopよりも遅く,アルゴリズム効率テストで影響を受けることがある.
では、JavaのストリームAPIはfor loopより遅いのはなぜですか.ストリームの代わりにいつがいいのでしょうか.
流れとは?

  • ストリームは、関数プログラミング言語の順序(=taskの順序)と同じ用語です.関数をパラメータに順番に渡す動作をシーケンスプログラミングと呼ぶ.

  • ストリームは、内部歪みPattern(=内部反復モード)を使用します.
    内部反復者モードとは,集合内部で要素を反復し,開発者が各要素が処理するコードのみを提供するコードモードである.すなわち,ストリーム内部を巡回し,クライアントの立場では反復論理を管理するだけでよい.

  • Fluent ProgrammingまたはFluent APIと呼ばれています.
  •  int sum = widgets.stream()
                          .filter(w -> w.getColor() == RED)
                          .mapToInt(w -> w.getWeight())
                          .sum();
     
    };
    Langerは,各サイクル,シーケンスフロー,並列フローの性能についてベンチマークテストを行った.
    forサイクルとシーケンスフロー
    for loop vsシーケンスフロー
    次に、配列内の最大要素を検索する関数を示します.
    // for-loop
    int[] a = ints;
    int e = ints.length;
    int m = Integer.MIN_VALUE;
    for (int i = 0; i < e; i++) {
        if (a[i] > m) {
            m = a[i];
        }
    }
    // sequential stream
    //reduce(T identity, BinaryOperator<T> accumulator)
    int m = Arrays.stream(ints).reduce(Integer.MIN_VALUE, Math::max);
    for-loop: 0.36 ms
    stream: 5.35 ms
    流れの速度が約15倍遅いことが確認できた.
    Langer氏によると、JIT Compilerがfor loopを処理して40年以上になるため、for loopの内部最適化はすでに良好だという.しかしストリームは2015年以降に導入されたため,コンパイラはまだ正確に最適化されていない.
    for loop vsシーケンスストリームから格納するデータ構造wrapped typeに変更
    ArrayListを作成し、最大の要素を返すために500000個のIntegerタイプを保存します.
    for-loop: 6.55ms
    stream: 8.33ms
    違いが明らかであることは間違いない.巡回ArrayListの料金自体が高く,両者の性能差を圧倒している.
    元のタイプとは異なり、パッケージタイプはスタック(直接参照)ではなくスタック(間接参照)メモリ領域に格納されます.
    間接参照のコストは直接参照のコストよりはるかに高いため,反復コスト自体が高く,最終的にfor loopのコンパイラ最適化の利点は消失する.
    各要素の計算コストの向上
    slowSin():パラメータに渡されるメソッドの識別関数値とそのテイラー級数を計算する関数.
    // for-loop
    int[] a = ints;
    int e = a.length;
    double m = Double.MIN_VALUE;
    for (int i = 0; i < e; i++) {
         double d = Sine.slowSin(a[i]);
         if (d > m) m = d;
    }
    // sequential stream
    Arrays.stream(ints).mapToDouble(Sine::slowSin).reduce(Double.MIN_VALUE, Math::max);
    for-loop: 11.72ms
    stream: 11.85ms
    for-loopが速くないことを確認できます.
    これは,関数内部の時間的複雑さが十分大きい場合,streamを用いてfor loopに対して速度損失をもたらさないことを示した.
    要約:「反復コストと機能コストの和が十分大きい場合、シーケンスフローの速度はforサイクルに近づきます.」
    シーケンスフローとパラレルフロー
    シーケンスフローは、1つのスレッドですべての重複を実行することを意味します.シーケンスフローは単一スレッドを使用するため,共有リソースの問題を考慮する必要はなく,CPUコアリソースを十分に利用する必要もない.
    逆に、パラレルストリームは、複数のスレッドで繰り返し実行されます.マルチスレッドにより、共有リソースの同期に問題が発生します.
    Javaのマルチスレッド実装チェーンParallelStream()はスレッドのセキュリティを保証しないため,作業中の開発者にとっては個別の処理が必要である.

    パラレルスレッドは、シーケンススレッドよりもオーバーヘッドが必要であることは明らかです.
    fork-joinタスク・オブジェクトの作成、ジョブの分割、スレッド・プールのスケジュールの管理、および共通プールを使用してオブジェクトを再使用すると、Garbage Collectorが書き込まれていないオブジェクトをクリーンアップするコストが発生します.
    このようなオーバーヘッドを負担しても、パラレルストリームが優位にある場合はParallelStreamを使用する必要があります.
    シーケンスフローとパラレルフロー
    500000個の数字を含んで、最大の要素を探します.テストはintタイプとIntegerタイプのArrayListを使用して行います.
    // sequential stream
    int m = Arrays.stream(ints).reduce(Integer.MIN_VALUE, Math::max);
    int m = myCollection.stream().reduce(Integer.MIN_VALUE, Math::max);
    // parallel stream
    int m = Arrays.stream(ints).parallel().reduce(Integer.MIN_VALUE, Math::max);
    int m = myCollection.parallelStream().reduce(Integer.MIN_VALUE, Math::max);
    int-Array: seq : 5.35ms
    int-Array: par : 3.35ms
    ArrayList: seq : 8.33ms
    ArrayList: par : 6.33ms
    LinkedList: seq :12.74ms
    LinkedList: par :19.57ms
    パラレルフローはシーケンスフローよりも速いが,差は劇的ではない.これは,計算費用が小さく,並列処理によりスレッドを分割する費用が高いためである.LinkedListの場合は,並列処理では分割作業が困難であるため遅い.
    各要素の計算コストの向上
    上で使用したslowSin()を使用します.
    // for-loop
    Arrays.stream(ints).parallel().mapToDouble(Sine::slowSin ) .reduce(Double.MIN_VALUE, (i, j) -> Math.max(i, j);
    // collection
    myCollection.parallelStream().mapToDouble(Sine::slowSin ) .reduce(Double.MIN_VALUE, (i, j) -> Math.max(i, j);
    int-Array: seq : 10.81ms
    int-Array: par : 6.03ms
    ArrayList: seq : 10.97ms
    ArrayList: par : 6.10ms
    LinkedList: seq :11.15ms
    LinkedList: par :6.25ms
    計算コストが上昇するにつれて,並列ストリームの速度は確かに1.8倍に向上した.
    for loopとstreamを適切に使用するためには,想像以上にデータ構造,タイプ,計算コスト,データ数を考慮する必要があることが分かった.
    それでもstreamが持つ「読み取り可能性」も考慮されていると思いますが、実際のアプリケーション開発では、1~2秒のパフォーマンスが重要性に依存する可能性があります.