Java 8:ストリームとコレクションのパフォーマンス


140

Java 8は初めてです。APIの詳細はまだわかりませんが、新しい非公式のベンチマークを作成して、新しいStreams APIと古き良きコレクションのパフォーマンスを比較しました。

このテストでは、のリストをフィルタリングし、Integer偶数ごとに平方根を計算しての結果Listに格納しDoubleます。

これがコードです:

    public static void main(String[] args) {
        //Calculating square root of even numbers from 1 to N       
        int min = 1;
        int max = 1000000;

        List<Integer> sourceList = new ArrayList<>();
        for (int i = min; i < max; i++) {
            sourceList.add(i);
        }

        List<Double> result = new LinkedList<>();


        //Collections approach
        long t0 = System.nanoTime();
        long elapsed = 0;
        for (Integer i : sourceList) {
            if(i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


        //Stream approach
        Stream<Integer> stream = sourceList.stream();       
        t0 = System.nanoTime();
        result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


        //Parallel stream approach
        stream = sourceList.stream().parallel();        
        t0 = System.nanoTime();
        result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
        elapsed = System.nanoTime() - t0;       
        System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));      
    }.

そして、これがデュアルコアマシンの結果です:

    Collections: Elapsed time:        94338247 ns   (0,094338 seconds)
    Streams: Elapsed time:           201112924 ns   (0,201113 seconds)
    Parallel streams: Elapsed time:  357243629 ns   (0,357244 seconds)

この特定のテストでは、ストリームの速度はコレクションの約2倍であり、並列処理は役に立ちません(または、間違った方法で使用していますか?)。

質問:

  • このテストは公正ですか?間違えましたか?
  • ストリームはコレクションよりも遅いですか?誰かがこれについて正式なベンチマークを作成しましたか?
  • どちらのアプローチをとるべきですか?

結果を更新しました。

@pveentjerの助言に従って、JVMウォームアップ(1k回の反復)の後にテストを1,000回実行しました。

    Collections: Average time:      206884437,000000 ns     (0,206884 seconds)
    Streams: Average time:           98366725,000000 ns     (0,098367 seconds)
    Parallel streams: Average time: 167703705,000000 ns     (0,167704 seconds)

この場合、ストリームのパフォーマンスが向上します。フィルタリング関数が実行時に1回または2回だけ呼び出されるアプリで何が観察されるのでしょうか。


1
IntStream代わりに試してみましたか?
Mark Rotteveel、2014年

2
ちゃんと測定していただけますか?あなたがしているすべてが1回の実行であるならば、あなたのベンチマークはもちろんオフになります。
skiwi 2014年

2
@MisterSmith 1Kテストでも、JVMをどのようにウォームアップしたかについて透明性を確保できますか
skiwi 2014年

1
そして、正しいマイクロベンチマークを足すに興味のある人のために、質問をHERESに:stackoverflow.com/questions/504103/...
ミスター・スミス

2
@assylias Using toListは、異なるスレッドがマージされる前にスレッド限定の中間リストに収集されるため、非スレッドセーフリストに収集している場合でも、並列で実行する必要があります。
スチュアートマークス

回答:


192
  1. LinkedListイテレータを使用してリストの真ん中から削除する以外は何も使用しないでください。

  2. 手動でベンチマークコードを記述するのをやめ、JMHを使用します。

適切なベンチマーク:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
    public static final int N = 10000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        for (int i = 0; i < N; i++) {
            sourceList.add(i);
        }
    }

    @Benchmark
    public List<Double> vanilla() {
        List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
        for (Integer i : sourceList) {
            if (i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        return result;
    }

    @Benchmark
    public List<Double> stream() {
        return sourceList.stream()
                .filter(i -> i % 2 == 0)
                .map(Math::sqrt)
                .collect(Collectors.toCollection(
                    () -> new ArrayList<>(sourceList.size() / 2 + 1)));
    }
}

結果:

Benchmark                   Mode   Samples         Mean   Mean error    Units
StreamVsVanilla.stream      avgt        10       17.588        0.230    ns/op
StreamVsVanilla.vanilla     avgt        10       10.796        0.063    ns/op

予想通り、ストリームの実装はかなり遅いです。JITはすべてのラムダ要素をインライン化できますが、バニラバージョンほど完全に簡潔なコードは生成しません。

一般に、Java 8ストリームは魔法ではありません。彼らはすでに十分に実装されているものを高速化することはできませんでした(おそらく、単純な反復、またはJava 5のfor-eachステートメントがIterable.forEach()andで置き換えられ、Collection.removeIf()呼び出しが行われています)。ストリームは、コーディングの利便性と安全性に関するものです。利便性-速度のトレードオフがここで機能しています。


2
時間を取っていただきありがとうございます。ArrayListのLinkedListを変更しても何も変更されないと思います。両方のテストで追加されるため、時間に影響はありません。とにかく、結果を説明してもらえますか?ここで何を測定しているのかわかりません(単位はns / opですが、opとは何ですか?)。
ミスタースミス

52
パフォーマンスについてのあなたの結論は有効ですが、誇張されています。要素ごとのアクセスコストがプレーンイテレータよりもストリームの方が安いため、主にストリームコードが反復コードより高速であるケースはたくさんあります。そして、多くの場合、ストリームバージョンは手書きバージョンと同等のものにインライン化します。もちろん、悪魔は詳細にあります。コードの特定のビットは異なる動作をする場合があります。
Brian Goetz 2014年

26
@BrianGoetz、ストリームが高速な場合の使用例を指定していただけますか?
Alexandr

1
使用:FMHの最後のバージョンで@Benchmarkはなく、@GenerateMicroBenchmark
pdem

3
@ BrianGoetz、Streamsの方が高速なユースケースを指定できますか?
kiltek 2018年

17

1)ベンチマークを使用すると、1秒未満の時間がわかります。つまり、結果には副作用の強い影響がある可能性があります。だから、私はあなたの仕事を10倍に増やしました

    int max = 10_000_000;

ベンチマークを実行しました。私の結果:

Collections: Elapsed time:   8592999350 ns  (8.592999 seconds)
Streams: Elapsed time:       2068208058 ns  (2.068208 seconds)
Parallel streams: Elapsed time:  7186967071 ns  (7.186967 seconds)

編集なし(int max = 1_000_000)結果は

Collections: Elapsed time:   113373057 ns   (0.113373 seconds)
Streams: Elapsed time:       135570440 ns   (0.135570 seconds)
Parallel streams: Elapsed time:  104091980 ns   (0.104092 seconds)

それはあなたの結果のようなものです:ストリームはコレクションより遅いです。結論:ストリームの初期化/値の送信に多くの時間が費やされました。

2)タスクストリームを増やした後、ストリームは速くなりました(それで問題ありません)が、並列ストリームは非常に遅いままでした。どうしましたか?注:あなたはcollect(Collectors.toList())あなたのコマンドを持っています。単一のコレクションへの収集は、基本的に、同時実行の場合にパフォーマンスのボトルネックとオーバーヘッドをもたらします。を置き換えることにより、オーバーヘッドの相対コストを推定することが可能です

collecting to collection -> counting the element count

ストリームの場合は、で実行できますcollect(Collectors.counting())。私は結果を得ました:

Collections: Elapsed time:   41856183 ns    (0.041856 seconds)
Streams: Elapsed time:       546590322 ns   (0.546590 seconds)
Parallel streams: Elapsed time:  1540051478 ns  (1.540051 seconds)

それは大きな仕事です!(int max = 10000000結論:コレクションへのアイテムの収集には、ほとんどの時間がかかりました。最も遅い部分はリストへの追加です。ところで、simple ArrayListはに使用されCollectors.toList()ます。


このテストをマイクロベンチマークする必要があります。つまり、最初に何度もウォームアップしてから、多数のTMEを実行して平均化する必要があります。
skiwi 2014年

@skiwi確かに、あなたは正しい、特に測定値に大きな偏差があるため。私は基本的な調査のみを行っており、結果を正確にするつもりはありません。
セルゲイフェドロフ2014年

サーバーモードのJITは、1万回の実行後に起動します。そして、コードをコンパイルして入れ替えるには時間がかかります。
pveentjer 2014年

この文について:「あなたはcollect(Collectors.toList())あなたの命令を持っています。つまり、単一のコレクションを多くのスレッドで処理する必要がある状況があるかもしれません。toList複数の異なるリストインスタンスを並行して収集していることはほぼ確実です。コレクションの最後のステップとしてのみ、要素は1つのリストに転送されて返されます。したがって、同期のオーバーヘッドはないはずです。これが、コレクターがサプライヤー、アキュムレーター、コンバイナーの両方の機能を備えている理由です。(もちろん、他の理由で遅くなる可能性があります)
Lii

@Lii collectここでの実装についても同じように思います。しかし、結局、いくつかのリストを1つのリストにマージする必要があります。この例では、マージが最も重い操作のように見えます。
セルゲイフェドロフ2016

4
    public static void main(String[] args) {
    //Calculating square root of even numbers from 1 to N       
    int min = 1;
    int max = 10000000;

    List<Integer> sourceList = new ArrayList<>();
    for (int i = min; i < max; i++) {
        sourceList.add(i);
    }

    List<Double> result = new LinkedList<>();


    //Collections approach
    long t0 = System.nanoTime();
    long elapsed = 0;
    for (Integer i : sourceList) {
        if(i % 2 == 0){
            result.add( doSomeCalculate(i));
        }
    }
    elapsed = System.nanoTime() - t0;       
    System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


    //Stream approach
    Stream<Integer> stream = sourceList.stream();       
    t0 = System.nanoTime();
    result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
            .collect(Collectors.toList());
    elapsed = System.nanoTime() - t0;       
    System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));


    //Parallel stream approach
    stream = sourceList.stream().parallel();        
    t0 = System.nanoTime();
    result = stream.filter(i -> i%2 == 0).map(i ->  doSomeCalculate(i))
            .collect(Collectors.toList());
    elapsed = System.nanoTime() - t0;       
    System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));      
}

static double doSomeCalculate(int input) {
    for(int i=0; i<100000; i++){
        Math.sqrt(i+input);
    }
    return Math.sqrt(input);
}

コードを少し変更して、8コアのMac Book Proで実行したところ、妥当な結果が得られました。

コレクション:経過時間:1522036826 ns(1.522037秒)

ストリーム:経過時間:4315833719 ns(4.315834秒)

並列ストリーム:経過時間:261152901 ns(0.261153秒)


私はあなたのテストは公平だと思います、あなたはもっと多くのCPUコアを持つマシンが必要です。
Mellon

3

あなたがやろうとしていることのために、私はとにかく通常のJava APIを使用しません。大量のボクシング/アンボクシングが行われているため、パフォーマンスのオーバーヘッドが非常に大きくなります。

個人的には、設計されたAPIの多くはオブジェクトのゴミを大量に作成するため、がらくたであると思います。

double / intのプリミティブ配列を使用してシングルスレッドで実行し、パフォーマンスを確認してください。

PS:ベンチマークを実行するためにJMHを確認することをお勧めします。これは、JVMのウォームアップなどの典型的な落とし穴のいくつかを処理します。


LinkedListsは、すべてのノードオブジェクトを作成する必要があるため、ArrayListsよりもさらに劣ります。modオペレーターも非常に遅いです。私は10/15サイクルのようなものを信じています+それは命令パイプラインを排出します。2で非常に高速な除算を行う場合は、数値を1ビット右にシフトします。これらは基本的なトリックですが、スピードを上げるためのモードアドバンストリックがあると確信していますが、これらはおそらくより問題固有のものです。
pveentjer 2014年

私はボクシングを知っています。これは単なる非公式のベンチマークです。アイデアは、コレクションとストリームテストの両方で、同じ量のボクシング/アンボクシングを行うことです。
ミスタースミス

最初に、測定ミスではないことを確認します。実際のベンチマークを実行する前に、ベンチマークを数回実行してみてください。その後、少なくともJVMウォームアップが邪魔にならず、コードは正しくJITTEDされます。これがないと、おそらく間違った結論を下します。
pveentjer 14年

わかりました。アドバイスに従って新しい結果を投稿します。私はJMHを確認しましたが、Mavenが必要であり、設定に時間がかかります。とにかくありがとう。
ミスタースミス

「何をしようとしているのか」という観点からベンチマークテストを考えるのは避けた方がいいと思います。つまり、通常、これらの種類のエクササイズは、実証できるほど単純化されていますが、単純化できる/する必要があるように見えるほど複雑です。
ryvantage 2014年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.