web-dev-qa-db-ja.com

より効率的なものは何ですか:ソートされたストリームまたはリストのソート?

コレクションにいくつかのアイテムがあり、リストの結果を期待して、特定のコンパレーターを使用してそれらをソートしたいとします:

Collection<Item> items = ...;
Comparator<Item> itemComparator = ...;

アプローチの1つは、次のようなリストの項目をソートすることです。

List<Item> sortedItems = new ArrayList<>(items);
Collections.sort(sortedItems, itemComparator);

別のアプローチは、ソートされたストリームを使用しています:

List<Item> sortedItems = items
    .stream()
    .sorted(itemComparator)
    .collect(Collectors.toList());

どちらの方法がより効率的ですか?ソートされたストリームの利点はありますか(複数コアでの高速ソートなど)。

ランタイムの複雑さ/最速という意味で効率的です。

私は完璧な benchmark を実装することを信じていません。そしてSortedOpsを勉強しても本当に悟りはありませんでした。

17
lexicore

コードを見なくても、2つの形式のソートは同じ複雑さを持つと言っても安全です。 (そうしなかった場合、1つのフォームが大幅に破損します!)

Java 8ストリームのソースコード(具体的には内部クラス_Java.util.stream.SortedOps_))を見ると、sorted()メソッドは、すべての要素を配列またはArrayListにストリームします。

  • 配列は、パイプラインアセンブリコードが事前にストリーム内の要素数を推定できる場合にのみ使用されます。

  • それ以外の場合は、ArrayListを使用して、ソートする要素を収集します。

ArrayListが使用されている場合、リストの作成/拡大の追加のオーバーヘッドが発生します。

次に、2つのバージョンのコードに戻ります。

_List<Item> sortedItems = new ArrayList<>(items);
Collections.sort(sortedItems, itemComparator);
_

このバージョンでは、ArrayListコンストラクターが要素itemsを適切なサイズの配列にコピーし、次に_Collections.sort_がその配列のインプレースソートを実行します。 (これは内部で起こります)。

_List<Item> sortedItems = items
    .stream()
    .sorted(itemComparator)
    .collect(Collectors.toList());
_

このバージョンでは、上記で見たように、sorted()に関連付けられたコードは配列を構築して並べ替えるか(上記と同じです)、またはArrayListを低速で構築します。しかしその上に、itemsからコレクターへのデータのストリームのオーバーヘッドがあります。

全体(少なくともJava 8実装)の場合)コード検査では、コードの最初のバージョンが2番目のバージョンより遅くなることはなく、ほとんどの場合(すべてではないが)速くなることがわかります。しかし、リストが大きくなると、O(NlogN)のソートがコピーのO(N)オーバーヘッドを支配する傾向があります。つまり、2つの間のrelativeの違いバージョンは小さくなります。

本当に気になる場合は、Javaの特定の実装と特定の入力データセットとの実際の違いをテストするためのベンチマークを記述できるはずです。 (または@Eugeneのベンチマークを適応させてください!)

9
Stephen C

正直言って、私は自分自身を信用していません多すぎるJMHでも(私の場合、多くの時間がかかるアセンブリを理解していない限り)、特に@Setup(Level.Invocation)ですが、ここに小さなテストがあります(私が行った他のテストからStringInput生成を取得しましたが、問題にはなりません。ソートするのは単なるデータです)

@State(Scope.Thread)
public static class StringInput {

    private String[] letters = { "q", "a", "z", "w", "s", "x", "e", "d", "c", "r", "f", "v", "t", "g", "b",
            "y", "h", "n", "u", "j", "m", "i", "k", "o", "l", "p" };

    public String s = "";

    public List<String> list;

    @Param(value = { "1000", "10000", "100000" })
    int next;

    @TearDown(Level.Invocation)
    public void tearDown() {
        s = null;
    }

    @Setup(Level.Invocation)
    public void setUp() {

         list = ThreadLocalRandom.current()
                .ints(next, 0, letters.length)
                .mapToObj(x -> letters[x])
                .map(x -> Character.toString((char) x.intValue()))
                .collect(Collectors.toList());

    }
}


@Fork(1)
@Benchmark
public List<String> testCollection(StringInput si){
    Collections.sort(si.list, Comparator.naturalOrder());
    return si.list;
}

@Fork(1)
@Benchmark
public List<String> testStream(StringInput si){
    return si.list.stream()
            .sorted(Comparator.naturalOrder())
            .collect(Collectors.toList());
}

結果はCollections.sortは高速ですが、大幅なマージンではありません。

Benchmark                                 (next)  Mode  Cnt   Score   Error  Units
streamvsLoop.StreamVsLoop.testCollection    1000  avgt    2   0.038          ms/op
streamvsLoop.StreamVsLoop.testCollection   10000  avgt    2   0.599          ms/op
streamvsLoop.StreamVsLoop.testCollection  100000  avgt    2  12.488          ms/op
streamvsLoop.StreamVsLoop.testStream        1000  avgt    2   0.048          ms/op
streamvsLoop.StreamVsLoop.testStream       10000  avgt    2   0.808          ms/op
streamvsLoop.StreamVsLoop.testStream      100000  avgt    2  15.652          ms/op
12
Eugene

以下は私のベンチマークです(それが正しいかどうかは本当にわかりません):

import Java.util.ArrayList;
import Java.util.Collections;
import Java.util.List;
import Java.util.Set;
import Java.util.TreeSet;
import Java.util.concurrent.TimeUnit;
import Java.util.stream.Collectors;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(MyBenchmark.N)
public class MyBenchmark {

    public static final int N = 50;

    public static final int SIZE = 100000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        System.out.println("Generating the list");
        for (int i = 0; i < SIZE; i++) {
            sourceList.add(i);
        }
        System.out.println("Shuffling the list.");
        Collections.shuffle(sourceList);
    }

    @Benchmark
    public List<Integer> sortingList() {
        List<Integer> sortedList = new ArrayList<>(sourceList);
        Collections.sort(sortedList);
        return sortedList;
    }

    @Benchmark
    public List<Integer> sortedStream() {
        List<Integer> sortedList = sourceList.stream().sorted().collect(Collectors.toList());
        return sortedList;
    }

    @Benchmark
    public List<Integer> treeSet() {
        Set<Integer> sortedSet = new TreeSet<>(sourceList);
        List<Integer> sortedList = new ArrayList<>(sortedSet);
        return sortedList;
    }
}

結果:

Benchmark                 Mode  Cnt       Score       Error  Units
MyBenchmark.sortedStream  avgt  200  300691.436 ± 15894.717  ns/op
MyBenchmark.sortingList   avgt  200  262704.939 ±  5073.915  ns/op
MyBenchmark.treeSet       avgt  200  856577.553 ± 49296.565  ns/op

@Eugeneのベンチマークと同様に、並べ替えリストは、並べ替えられたストリームよりもわずかに(約20%)高速です。私を少し驚かせたのは、treeSetが大幅に遅いということです。それを予想しなかった。

1
lexicore