2016-12-19 7 views
12

いくつかのプロファイリング結果を見てみると、密なループ(別のネストされたループの代わりに使用される)でストリームを使用すると、タイプjava.util.stream.ReferencePipelinejava.util.ArrayList$ArrayListSpliteratorのオブジェクトに大きなメモリオーバーヘッドが発生しました。問題のストリームをforeachループに変換し、メモリ消費量が大幅に減少しました。Java 8ストリームオブジェクト重要なメモリ使用

ストリームは通常のループよりも優れたパフォーマンスを約束するものではありませんが、その差はごくわずかです。この場合、それは40%の増加だったようです。

ここでは、問題を特定するために書いたテストクラスを示します。私はJFRとメモリ消費とオブジェクトの割り当てを監視さ:

import java.util.ArrayList; 
import java.util.List; 
import java.util.Optional; 
import java.util.Random; 
import java.util.function.Predicate; 

public class StreamMemoryTest { 

    private static boolean blackHole = false; 

    public static List<Integer> getRandListOfSize(int size) { 
     ArrayList<Integer> randList = new ArrayList<>(size); 
     Random rnGen = new Random(); 
     for (int i = 0; i < size; i++) { 
      randList.add(rnGen.nextInt(100)); 
     } 
     return randList; 
    } 

    public static boolean getIndexOfNothingManualImpl(List<Integer> nums, Predicate<Integer> predicate) { 

     for (Integer num : nums) { 
      // Impossible condition 
      if (predicate.test(num)) { 
       return true; 
      } 
     } 
     return false; 
    } 

    public static boolean getIndexOfNothingStreamImpl(List<Integer> nums, Predicate<Integer> predicate) { 
     Optional<Integer> first = nums.stream().filter(predicate).findFirst(); 
     return first.isPresent(); 
    } 

    public static void consume(boolean value) { 
     blackHole = blackHole && value; 
    } 

    public static boolean result() { 
     return blackHole; 
    } 

    public static void main(String[] args) { 
     // 100 million trials 
     int numTrials = 100000000; 
     System.out.println("Beginning test"); 
     for (int i = 0; i < numTrials; i++) { 
      List<Integer> randomNums = StreamMemoryTest.getRandListOfSize(100); 
      consume(StreamMemoryTest.getIndexOfNothingStreamImpl(randomNums, x -> x < 0)); 
      // or ... 
      // consume(StreamMemoryTest.getIndexOfNothingManualImpl(randomNums, x -> x < 0)); 
      if (randomNums == null) { 
       break; 
      } 
     } 
     System.out.print(StreamMemoryTest.result()); 
    } 
} 

ストリーム実装:

Memory Allocated for TLABs 64.62 GB

Class Average Object Size(bytes) Total Object Size(bytes) TLABs Average TLAB Size(bytes) Total TLAB Size(bytes) Pressure(%) 
java.lang.Object[]       415.974 6,226,712 14,969 2,999,696.432 44,902,455,888 64.711 
java.util.stream.ReferencePipeline$2  64  131,264  2,051 2,902,510.795 5,953,049,640 8.579 
java.util.stream.ReferencePipeline$Head  56  72,744  1,299 3,070,768.043 3,988,927,688 5.749 
java.util.stream.ReferencePipeline$2$1  24  25,128  1,047 3,195,726.449 3,345,925,592 4.822 
java.util.Random       32  30,976  968  3,041,212.372 2,943,893,576 4.243 
java.util.ArrayList       24  24,576  1,024 2,720,615.594 2,785,910,368 4.015 
java.util.stream.FindOps$FindSink$OfRef  24  18,864  786  3,369,412.295 2,648,358,064 3.817 
java.util.ArrayList$ArrayListSpliterator 32  14,720  460  3,080,696.209 1,417,120,256 2.042 

マニュアル実装:

Memory Allocated for TLABs 46.06 GB

Class Average Object Size(bytes) Total Object Size(bytes) TLABs Average TLAB Size(bytes) Total TLAB Size(bytes) Pressure(%) 
java.lang.Object[]  415.961  4,190,392  10,074  4,042,267.769  40,721,805,504 82.33 
java.util.Random  32   32,064   1,002  4,367,131.521  4,375,865,784 8.847 
java.util.ArrayList  24   14,976   624   3,530,601.038  2,203,095,048 4.454 

メモリを消費するストリームオブジェクト自体に問題が発生しましたか? /これは既知の問題ですか?

+6

はい、これは完全に予想されます。ストリームのオーバーヘッドは、このような小さな入力に対しては明らかに重要です。 –

+4

正確には関係ないが、 'getIndexOfNothingManualImpl'は' nums.stream()を返すanyMatch(述語) 'と等価ではないでしょうか? – Zircon

+0

はい、あなたは正しいです。以前のバージョンでは、実際に返された値で遊んでいましたが、その時点で変更するつもりはありませんでした。 –

答えて

4

Stream APIを構築するために必要なインフラストラクチャが原因で、メモリが増えるだけでなく、しかし、それはかもしれない(少なくともこの小さな入力のために)速度の点で遅くなるかもしれません。

実行速度が30%悪化している(それほど複雑ではないが)簡単な例を示すOracleの開発者からのプレゼンテーションがあります(それはロシア語ですが、それはポイントです)。 StreamsとLoopsの場合彼はそれはかなり正常だと言います。

Streams(ラムダとメソッド参照をより正確にする)を使用すると、わからない多くのクラスが(潜在的に)作成されることに、多くの人が気づいていないことがあります。

てみて、あなたの例を実行するには:

-Djdk.internal.lambda.dumpProxyClasses=/Some/Path/Of/Yours 

そして

+1

技術的には、理想的な状況下では、スプライテータは同時変更チェックを1回しか実行する必要がないため、ストリームはアレイリストのほうが高速になります。 – the8472

+0

dumpProxyClassesはストリームとは何の関係もなく、ラムダのランタイム表現の内部実装です。ストリームなしでlambdaを使用すると(OPのように)、それらも同様に使用できます。 –

+0

@ TagirValeevもちろん、あなたはまだプロキシクラスを使用するでしょう、私はより明確に言っておくべきです、コメントのためのthxは、編集します。 – Eugene

7

あなたが実際に割り当てるストリームAPIを使用してコードとコード(ASM経由)ストリームの必要性によって作成されますどのように多くの追加クラスを参照しますあなたの実験的な設定はやや疑わしいですが、より多くのメモリ。私はJFRを使ったことはありませんでしたが、JOLを使った私の知見はあなたと非常によく似ています。

ArrayListのクエリ中に割り当てられたヒープだけでなく、その作成および作成中にもヒープが割り当てられることに注意してください。割り当てと単一ArrayListの人口の間の配分は(JOLを経て、OOPS圧縮64-ビット、)次のようになります。

COUNT  AVG  SUM DESCRIPTION 
    1  416  416 [Ljava.lang.Object; 
    1  24  24 java.util.ArrayList 
    1  32  32 java.util.Random 
    1  24  24 java.util.concurrent.atomic.AtomicLong 
    4     496 (total) 

だから割り当てられたほとんどのメモリは、データを格納するためにArrayList内部で使用Object[]配列です。 AtomicLongは、ランダムクラス実装の一部です。これを100回実行すると、両方のテストで少なくとも496*10^8/2^30 = 46.2 Gbが割り当てられます。それにもかかわらず、この部分は両方のテストで同一である必要があるため、スキップすることができます。

もう1つ興味深いのはインライン展開です。 JITは全体getIndexOfNothingManualImpljava -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining StreamMemoryTest経由)インライン化するのに十分なスマートです:

StreamMemoryTest::main @ 13 (59 bytes) 
    ... 
    @ 30 StreamMemoryTest::getIndexOfNothingManualImpl (43 bytes) inline (hot) 
     @ 1 java.util.ArrayList::iterator (10 bytes) inline (hot) 
     \-> TypeProfile (2132/2132 counts) = java/util/ArrayList 
     @ 6 java.util.ArrayList$Itr::<init> (6 bytes) inline (hot) 
      @ 2 java.util.ArrayList$Itr::<init> (26 bytes) inline (hot) 
      @ 6 java.lang.Object::<init> (1 bytes) inline (hot) 
     @ 8 java.util.ArrayList$Itr::hasNext (20 bytes) inline (hot) 
     \-> TypeProfile (215332/215332 counts) = java/util/ArrayList$Itr 
     @ 8 java.util.ArrayList::access$100 (5 bytes) accessor 
     @ 17 java.util.ArrayList$Itr::next (66 bytes) inline (hot) 
     @ 1 java.util.ArrayList$Itr::checkForComodification (23 bytes) inline (hot) 
     @ 14 java.util.ArrayList::access$100 (5 bytes) accessor 
     @ 28 StreamMemoryTest$$Lambda$1/791452441::test (8 bytes) inline (hot) 
     \-> TypeProfile (213200/213200 counts) = StreamMemoryTest$$Lambda$1 
     @ 4 StreamMemoryTest::lambda$main$0 (13 bytes) inline (hot) 
      @ 1 java.lang.Integer::intValue (5 bytes) accessor 
     @ 8 java.util.ArrayList$Itr::hasNext (20 bytes) inline (hot) 
     @ 8 java.util.ArrayList::access$100 (5 bytes) accessor 
    @ 33 StreamMemoryTest::consume (19 bytes) inline (hot) 

分解は、実際にイテレータのない割り当ては、ウォームアップ後に実行されていないことを示しています。エスケープ解析は、イテレータオブジェクトがエスケープしないことをJITに正しく伝えるため、単純にスカラー化されています。 JITも全く反復を削除することができることを

COUNT  AVG  SUM DESCRIPTION 
    1  32  32 java.util.ArrayList$Itr 
    1     32 (total) 

注:Iteratorは、実際にはさらに32バイトを取るだろう割り当てられていました。 blackholeはデフォルトではfalseであるため、に関係なくblackhole = blackhole && valueを変更しても、副作用がないため、valueの計算はまったく除外できます。私はそれが実際にこれをしたかどうかはわかりませんが(読解はかなり難しいですが)、それは可能です。

getIndexOfNothingStreamImplもストリームAPIの中に相互依存するオブジェクトが多すぎるためエスケープ解析が失敗するため、実際の割り当てが行われます。したがって、この特定のストリームのすべての呼び出しは、実際に200追加バイトを割り当て

COUNT  AVG  SUM DESCRIPTION 
    1  32  32 java.util.ArrayList$ArrayListSpliterator 
    1  24  24 java.util.stream.FindOps$FindSink$OfRef 
    1  64  64 java.util.stream.ReferencePipeline$2 
    1  24  24 java.util.stream.ReferencePipeline$2$1 
    1  56  56 java.util.stream.ReferencePipeline$Head 
    5     200 (total) 

:したがって、それは本当に5つの追加のオブジェクトを(テーブルはJOL出力から手動で構成されて)追加されます。 100_000_000回の繰り返しを実行すると、合計ストリームバージョンでは、結果に近い手動バージョンより10^8 * 200/2^30 = 18.62Gbを割り当てる必要があります。私は、の中のAtomicLongも同様にスカラー化されていますが、ウォームアップの反復中には(とAtomicLongの両方が存在すると考えられます(JITが実際に最も最適化されたバージョンを作成するまで)。これは数字の小さな違いを説明します。

この200バイトの追加割り当ては、ストリームサイズに依存しませんが、中間ストリーム操作の数に依存します(特に、追加のフィルタステップごとに64 + 24 = 88バイトを追加します)。しかし、これらのオブジェクトは通常短命であり、素早く割り当てられ、マイナーGCによって収集されることに注意してください。実際のアプリケーションのほとんどでは、これについて心配する必要はありません。

+0

すごい素晴らしい答え。 –

+0

説明のポイントとして、ストリームAPIの中に相互依存するオブジェクトが多すぎると言った場合、一般的にまたはこのシナリオを意味しますか? –

+1

@ BryanJ、両方。エスケープ解析は非常に壊れやすいものです。いくつかの知られているパターンを除いて少しのステップがあります。単に「これらのオブジェクトがメソッドをエスケープしないかどうかわかりません。例えば。 2つのオブジェクトを割り当ててお互いにリンクすると、エスケープしなくてもEAの現在の実装は間違いなく失敗します。 –

関連する問題