コレクションにいくつかのアイテムがあり、リストの結果を期待して、特定のコンパレーターを使用してそれらをソートしたいとします:
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
を勉強しても本当に悟りはありませんでした。
コードを見なくても、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のベンチマークを適応させてください!)
正直言って、私は自分自身を信用していません多すぎる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
以下は私のベンチマークです(それが正しいかどうかは本当にわかりません):
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
が大幅に遅いということです。それを予想しなかった。