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

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

私は Java 8 の初心者です。まだ API を詳しくは知りませんが、新しい Streams API と古き良き Collections のパフォーマンスを比較するための、簡単なベンチマークを作成しました。

テストは、 のリストをフィルタリングし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 回の反復) 後にテストを 1k 回実行しました。

    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

  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()andCollection.removeIf()呼び出しに置き換えたもの) を高速化することはできません。ストリームは、コーディングの利便性と安全性を重視しています。ここでは利便性と速度のトレードオフが機能しています。

おすすめ記事