假设我们在集合中有一些项目,我们希望使用某个比较器对它们进行排序,期望在列表中得到结果:
Collection<Item> items = ...;
Comparator<Item> itemComparator = ...;
其中一种方法是对列表中的项目进行排序,例如:
List<Item> sortedItems = new ArrayList<>(items);
Collections.sort(sortedItems, itemComparator);
另一种方法是使用排序流:
List<Item> sortedItems = items
.stream()
.sorted(itemComparator)
.collect(Collectors.toList());
我想知道,哪种方法效率更高?排序流是否有任何优点(如多核上的紧固排序)?
从运行时复杂度/最快的角度来看,效率很高。
我不相信自己能够实现一个完美的benchmark并且学习SortedOps
并没有真正启发我。
答案 0 :(得分:11)
说实话,我不相信自己太多在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
答案 1 :(得分:7)
可以肯定地说,两种排序形式具有相同的复杂性......即使不查看代码也是如此。 (如果他们没有,那么一个表格会严重破坏!)
查看流的Java 8源代码(特别是内部类java.util.stream.SortedOps
),sorted()
方法将一个组件添加到流管道中,该流管道将所有流元素捕获到一个数组或一个数组中ArrayList
。
当且仅当管道汇编代码可以提前推断出流中的元素数时,才使用数组。
否则,ArrayList
用于收集要排序的元素。
如果使用ArrayList
,则会产生构建/增长列表的额外开销。
然后我们返回两个版本的代码:
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实现)代码检查告诉我,代码的第一个版本不能比第二个版本慢,并且在大多数(如果不是全部)情况下它会更快。但随着列表变大,O(NlogN)
排序将主导复制的O(N)
开销。这意味着两个版本之间的相对差异会变小。
如果您真的在意,您应该能够编写基准来测试Java的特定实现和特定输入数据集的实际差异。 (或者适应@Eugene的基准!)
答案 2 :(得分:1)
以下是我的基准(不确定它是否正确):
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
明显变慢了。我没想到。