web-dev-qa-db-ja.com

Java 8のイテレータとストリーム

Jdk 8のJava.util.streamに含まれる幅広いクエリメソッドを利用するために、*多重度(0個以上のインスタンス)との関係のゲッターがStream<T>またはIterable<T>ではなくIterator<T>を返すドメインモデルを設計しようとしました。

私の疑問は、Stream<T>と比較してIterator<T>によって追加のオーバーヘッドが発生するかどうかです。

それで、Stream<T>を使用してドメインモデルを侵害することの欠点はありますか?

または代わりに、常にIterator<T>またはIterable<T>を返し、イテレータをStreamUtilsに変換することにより、ストリームを使用するかどうかを選択する決定をエンドユーザーに委ねるべきですか?

Collectionを返すことは有効なオプションではありません。この場合、ほとんどの関係が遅延しており、サイズが不明です。

43
Miguel Gamboa

ここにはパフォーマンスに関する多くのアドバイスがありますが、残念なことにその多くは当て推量であり、実際のパフォーマンスに関する考慮事項を示すものはほとんどありません。

@Holger 正しく パフォーマンスの尾がAPI設計犬を揺さぶる一見圧倒的な傾向に抵抗する必要があることを指摘することで。

どのような場合でも、ストリームを他の形式のトラバーサルよりも遅く、同じ、または速くできる無数の考慮事項がありますが、重要なのは、ストリームがパフォーマンス上の優位性を持っていることを示すいくつかの要因がありますデータセット。

Streamの作成と比較して、creatingIteratorの追加の固定スタートアップオーバーヘッドがいくつかあります-計算を開始する前のいくつかのオブジェクト。データセットが大きい場合は問題ではありません。それは多くの計算で償却される小さなスタートアップ費用です。 (そして、あなたのデータセットが小さいなら、それはおそらく重要ではありません-あなたのプログラムが小さなデータセットで動作しているなら、パフォーマンスは一般的にあなたの一番の関心事でもないからです。)これ問題は並行するときです。パイプラインのセットアップに費やした時間は、アムダールの法則の連続した部分になります。実装を見ると、ストリームのセットアップ中にオブジェクトのカウントダウンを維持するために一生懸命働きますが、並列が勝ち始める損益分岐点のデータセットのサイズに直接影響するので、それを減らす方法を見つけたいですシーケンシャル。

ただし、固定起動コストよりも重要なのは、要素ごとのアクセスコストです。ここでは、ストリームが実際に勝ちます-そしてしばしば大きく勝ちます-一部は驚くかもしれません。 (パフォーマンステストでは、対応するCollectionよりもforループを上回ることができるストリームパイプラインを定期的に確認します。)また、これについて簡単な説明があります。Spliteratorは、基本的に要素ごとのアクセスコストがIteratorよりも基本的に低く、順次でもです。これにはいくつかの理由があります。

  1. Iteratorプロトコルは基本的に効率が劣ります。各要素を取得するには、2つのメソッドを呼び出す必要があります。さらに、イテレータはnext()なしでhasNext()を呼び出す、またはhasNext()なしでnext()を複数回呼び出すなど、これらのメソッドの両方に対して堅牢でなければならないため、一般に、防御的なコーディング(および一般に、より多くのステートフル性と分岐)を行う必要があり、非効率性が増します。一方、スプリッテレーター(tryAdvance)をゆっくりと移動する方法でも、この負担はありません。 (next/hasNextの双対性は基本的に際どいため、同時データ構造の場合はさらに悪化します。また、Iteratorの実装は、Spliteratorの実装よりも多くの作業を行う必要があります。)

  2. Spliteratorは、「高速パス」反復(forEachRemaining)をさらに提供します。これは、ほとんどの場合(削減、forEach)使用でき、データ構造内部へのアクセスを仲介する反復コードのオーバーヘッドをさらに削減します。また、これは非常に適切にインライン化される傾向があり、コードモーション、境界チェックの除去など、他の最適化の有効性が向上します。

  3. さらに、Spliteratorを介したトラバースでは、Iteratorを使用した場合よりもヒープ書き込みが少なくなる傾向があります。 Iteratorを使用すると、すべての要素が1つまたは複数のヒープ書き込みを引き起こします(Iteratorをエスケープ分析でスカラー化して、そのフィールドをレジスターに引き上げる場合を除きます)。一方、Spliteratorsは状態が少ない傾向があり、産業用のforEachRemaining実装は、トラバーサルの終わりまでヒープへの書き込みを延期する傾向があり、代わりに、レジスターに自然にマップされる反復状態をローカルに保存して、メモリを削減しますバスアクティビティ。

要約:心配しないで、幸せになってください。 Spliteratorは、並列処理がなくても優れたIteratorです。 (また、一般的に書くのが簡単で、間違えにくいです。)

57
Brian Goetz

ソースがArrayListであると仮定して、すべての要素を反復する一般的な操作を比較しましょう。次に、これを達成するための3つの標準的な方法があります。

  • _Collection.forEach_

    _final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    _
  • _Iterator.forEachRemaining_

    _final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
        throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
        consumer.accept((E) elementData[i++]);
    }
    _
  • _Stream.forEach_呼び出してしまう _Spliterator.forEachRemaining_

    _if ((i = index) >= 0 && (index = hi) <= a.length) {
       for (; i < hi; ++i) {
           @SuppressWarnings("unchecked") E e = (E) a[i];
           action.accept(e);
       }
       if (lst.modCount == mc)
           return;
    }
    _

ご覧のとおり、これらの操作が終了する実装コードの内部ループは基本的に同じで、インデックスを反復処理し、配列を直接読み取り、Consumerに要素を渡します。

JREのすべての標準コレクションにも同様のことが当てはまります。読み取り専用のラッパーを使用している場合でも、それらはすべて、あらゆる方法で実装を適合させています。後者の場合、Stream AP​​Iはわずかに勝ちます。元のコレクションのforEachに委任するには、読み取り専用ビューで_Collection.forEach_を呼び出す必要があります。同様に、remove()メソッドを呼び出そうとする試みから保護するために、イテレーターをラップする必要があります。対照的に、spliterator()は変更のサポートがないため、元のコレクションのSpliteratorを直接返すことができます。したがって、読み取り専用ビューのストリームは、元のコレクションのストリームとまったく同じです。

前述のように、実際のパフォーマンスを測定する場合、これらのすべての違いに気付くことはほとんどありませんが、内部ループはすべての場合に同じ。

問題は、そこからどの結論を引き出すかです。呼び出し元は依然としてstream().forEach(…)を呼び出して元のコレクションのコンテキストで直接反復するため、読み取り専用のラッパービューを元のコレクションに返すことができます。

パフォーマンスに大きな違いはないため、 「コレクションまたはストリームを返す必要がありますか?」

14
Holger