並列無限Javaストリームがメモリ不足になる


16

次のJavaプログラムがを提供する理由を理解しようとしていますがOutOfMemoryError、対応するプログラムなしで.parallel()は対応していません。

System.out.println(Stream
    .iterate(1, i -> i+1)
    .parallel()
    .flatMap(n -> Stream.iterate(n, i -> i+n))
    .mapToInt(Integer::intValue)
    .limit(100_000_000)
    .sum()
);

2つの質問があります。

  1. このプログラムの意図する出力は何ですか?

    これがない.parallel()場合、これは単純に出力するように見えますsum(1+2+3+...)。つまり、flatMapの最初のストリームで単に「行き詰まる」ということです。これは理にかなっています。

    並列では、予想される動作があるかどうかはわかりませんが、それは何らかの形で最初のnストリームをインターリーブしたと思われます。ここnで、並列ワーカーの数です。また、チャンク/バッファリングの動作に基づいて少し異なる場合もあります。

  2. メモリが不足する原因は何ですか?具体的には、これらのストリームが内部でどのように実装されるかを理解しようとしています。

    何かがストリームをブロックしていると思うので、ストリームが終了せず、生成された値を取り除くことができますが、評価の順序とバッファリングが発生する場所がわかりません。

編集:必要に応じて、Java 11を使用しています。

Editt 2:どうやら同じことは単純なプログラムIntStream.iterate(1,i->i+1).limit(1000_000_000).parallel().sum()でも発生するので、limitではなくの怠惰に関係している可能性がありflatMapます。


parallel()は内部的にForkJoinPoolを使用します。ForkJoinフレームワークはJava 7からJavaにあると思います
aravind

回答:


9

しかし、どのような順序で評価され、どこでバッファリングが発生するのかはよくわかりません」と言っていますが、これがまさに並列ストリームに関するものです。評価の順序は不定です。

あなたの例の重要な側面は.limit(100_000_000)です。これは、実装が任意の値を合計するだけでなく、最初の100,000,000の数値を合計する必要があることを意味します。参照実装で.unordered().limit(100_000_000)は、結果は変更されないことに注意してください。これは、順序付けされていない場合の特別な実装がないことを示していますが、これは実装の詳細です。

ワーカースレッドが要素を処理するとき、特定のワークロードの前にある要素の数に応じて、どの要素が消費を許可されるかを知る必要があるため、ワーカースレッドが要素を処理することはできません。このストリームはサイズを知らないので、これは、プレフィックス要素が処理されたときにのみ知ることができます。これは無限ストリームでは決して起こりません。そのため、ワーカースレッドは当面の間バッファリングを続け、この情報が利用可能になります。

原則として、ワーカースレッドは、左端のワークチャンクを処理することを知っている場合、要素をすぐに合計し、それらをカウントして、制限に達したときに終了を通知できます。したがって、ストリームは終了する可能性がありますが、これは多くの要因に依存します。

あなたのケースでは、もっともらしいシナリオは、他のワーカースレッドが最も左側のジョブがカウントしているよりもバッファーの割り当てが速いということです。このシナリオでは、タイミングの微妙な変更により、ストリームが値を返す場合があります。

左端のチャンクを処理するスレッドを除くすべてのワーカースレッドをスローダウンすると、(少なくともほとんどの実行で)ストリームを終了させることができます。

System.out.println(IntStream
    .iterate(1, i -> i+1)
    .parallel()
    .peek(i -> { if(i != 1) LockSupport.parkNanos(1_000_000_000); })
    .flatMap(n -> IntStream.iterate(n, i -> i+n))
    .limit(100_000_000)
    .sum()
);

St 処理順序ではなく遭遇順序について話すときは、スチュアートマークスによる左から右の順序を使用するという提案に従っています。


とてもいい答えです!すべてのスレッドがflatMap操作の実行を開始するリスクがあり、実際にバッファーを空にする(合計する)スレッドが割り当てられないのではないでしょうか。私の実際の使用例では、無限ストリームは代わりにファイルが大きすぎてメモリに保持できません。メモリ使用量を抑えるためにストリームを書き換える方法を知りたいのですが。
Thomas Ahle

1
使っていFiles.lines(…)ますか?これは、Java 9で大幅に改善されました
ホルガー

1
これはJava 8での動作です。新しいJREではBufferedReader.lines()、特定の状況(デフォルトのファイルシステム、特別な文字セット、またはより大きいサイズではない)でもフォールバックしますInteger.MAX_FILES。これらのいずれかが当てはまる場合、カスタムソリューションが役立ちます。これは新しいQ&Aの価値があります…
ホルガー

1
Integer.MAX_VALUE、もちろん…
ホルガー

1
ファイルのストリームである外部ストリームとは何ですか?予測可能なサイズですか?
Holger

5

私の最高の推測では、追加することがあるということparallel()の内部動作に変更flatMap()され、既に持っていた問題をいい加減にする前に評価されているが

OutOfMemoryErrorあなたが取得しているが報告されたことを示すエラー[JDK-8202307] java.lang.OutOfMemoryErrorを取得:Stream.iteratorを呼び出すときにJavaのヒープ領域を()次の()flatMapに無限/非常に大きなストリームを使用してストリームに。。チケットを見ると、取得しているスタックトレースとほぼ同じです。次の理由により、修正されないためチケットはクローズされました。

iterator()そしてspliterator()、それは他の操作を使用することはできませんときの方法が使用されるように、「エスケープハッチ」です。ストリーム実装のプッシュモデルをプルモデルに変換するため、いくつかの制限があります。このような遷移では、要素が2つ以上の要素に(フラットに)マップされている場合など、特定の場合にバッファリングが必要です。ネストされた要素生成の層を通過する要素の数を伝達するためのバックプレッシャーの概念をサポートすることは、おそらく一般的なケースを犠牲にして、ストリームの実装を大幅に複雑にします。


これはとても面白いです!プッシュ/プル遷移は、メモリを使い果たす可能性があるバッファリングを必要とすることは理にかなっています。しかし私の場合、プッシュだけを使用しても問題なく機能し、残りの要素は表示されたとおりに破棄する必要がありますか?あるいは、フラップマップによってイテレータが作成されると言っているのでしょうか?
Thomas Ahle

3

OOMEが発生していないストリームであること無限ではなく、という事実によって、それはないです

つまり、をコメント化すると.limit(...)、メモリ不足になることはありませんが、もちろん終了することもありません。

分割されると、ストリームは要素の数を追跡できます。要素が各スレッド内に蓄積されている場合のみです(実際のアキュムレータはのようですSpliterators$ArraySpliterator#array)。

あなたがそれなしflatMapでそれを再現できるように見えます、単に以下を実行して-Xmx128mください:

    System.out.println(Stream
            .iterate(1, i -> i + 1)
            .parallel()
      //    .flatMap(n -> Stream.iterate(n, i -> i+n))
            .mapToInt(Integer::intValue)
            .limit(100_000_000)
            .sum()
    );

ただし、をコメントアウトした後はlimit()、ラップトップを節約することを決定するまで問題なく動作するはずです。

実際の実装の詳細に加えて、これが私が起こっていると思うものです:

を使用するlimitと、sumレデューサーは最初のX要素を合計する必要があるため、スレッドは部分合計を発行できません。各「スライス」(スレッド)は要素を蓄積し、それらを通過させる必要があります。制限がなければ、そのような制約はないので、各「スライス」は、最終的に結果を出力すると仮定して、取得した要素から部分的な合計を計算します(永久に)。


「分割されたら」とはどういう意味ですか?限界はどういうわけかそれを分割しますか?
Thomas Ahle

@ThomasAhle parallel()ForkJoinPool並列処理を実現するために内部的に使用します。これSpliteratorは、各ForkJoinタスクに作業を割り当てるために使用されます。ここでは、作業単位を「分割」と呼ぶことができると思います。
Karol Dowbecki

しかし、なぜそれが限界でのみ起こるのでしょうか?
Thomas Ahle

@ThomasAhle 2セントで答えを編集しました。
コスティCiudatu

1
@ThomasAhle Integer.sum()は、IntStream.sumレデューサーによって使用されるブレークポイントを設定します。制限なしのバージョンは常にその関数を呼び出すのに対し、制限付きのバージョンはOOMの前にそれを呼び出すことはありません。
コスティCiudatu
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.