什么更有效:排序流或排序列表?

Posted

技术标签:

【中文标题】什么更有效:排序流或排序列表?【英文标题】:What is more efficient: sorted stream or sorting a list? 【发布时间】:2018-09-22 16:47:46 【问题描述】:

假设我们在一个集合中有一些项目,我们想使用某个比较器对它们进行排序,期望结果是一个列表:

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 并没有真正启发我。

【问题讨论】:

好吧,至少有一个是就地排序的,没有办法让它并行(如果你有足够的数据来压缩一些性能) 你说的高效是什么意思?最快 = Collections.sort;最易读 = Stream;最高效的内存 = Collections.sort 也许 ... @OldCurmudgeon 最快。我已将其添加到问题中。 据我所知,两者都使用Arrays#sort @Eugene 正如赏金描述中所述,我想将其授予现有答案之一 - 你的。您努力实际编写基准测试,我认为值得额外赏金。 【参考方案1】:

可以肯定地说,两种形式的排序具有相同的复杂性……即使不查看代码。 (如果他们不这样做,那么一种形式将被严重破坏!)

查看流的 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】:

老实说,我对 JMH 也不太信任自己太多(除非我了解程序集,这在我的情况下需要很多时间),尤其是因为我使用过 @ 987654322@,但这里是一个小测试(我从我做的其他一些测试中提取了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

【讨论】:

我也开始写一个基准测试,看看我能得到什么。 “所以流有点慢。” - 正如预测的那样 :-) 我对创建列表的巴洛克式方式感到有点惊讶,连接字符串只是为了在之后拆分它们。 int[] letters = IntStream.range('A', 'Z').toArray(); List&lt;String&gt; random = IntStream.range(0, listSize) .mapToObj(ix -&gt; ThreadLocalRandom.current() .ints(ThreadLocalRandom.current().nextInt(1, maxWordSize), 0, letters.length) .map(i -&gt; letters[i]) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString()) .collect(Collectors.toCollection(ArrayList::new)); 怎么样? @Holger 不不,你是对的,它比丑陋的更丑,我在两次会议之间有 20 分钟的时间,必须构建 JMH 测试并运行它,所以我从我已经拥有的测试中获取它到位。顺便说一句,你所做的非常好:) 您已经有一个字符串。你试图在它上面调用intValue()(它不会编译),只是在它上面调用Character.toString。我想,我已经在其他地方看到了这个,但也许我错了。无论如何,.mapToObj(x -&gt; letters[x]) 产生一个String,因为letters 的类型为String[]。整个后续map 步骤已过时。【参考方案3】:

以下是我的基准(不确定是否正确):

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 明显变慢了。我没想到。

【讨论】:

在对一个完全不相关的问题/答案的评论中,Holger 曾经向我解释了为什么 TreeSet 更慢:添加到 TreeSet 是 O(log n),除非 sourceList 已排序通过相同的比较器,addAll 将简单地为每个元素调用add。简而言之,较慢的性能源于TreeSet 始终保持自身排序的事实。这会导致在添加许多元素时进行更多比较,而不是像在sortingList 中那样先添加所有元素然后排序。 @MalteHartwig 好吧,对列表进行排序也有 O(n×log n) 的最坏情况,但实际上,它介于 O(n) 和最坏情况之间。但请记住,插入树状图也意味着分配节点对象并可能重新平衡树。相比之下,new ArrayList&lt;&gt;(collection) 只分配和填充一个数组。 @lexicore 就我在这里看到的而言,存在一些问题,正确执行此操作的唯一方法是查看 JMH 样本(无数次 .. ),简单的测试做起来很简单,再复杂一点,它会变得有趣!这里所说的是我个人不喜欢的事情:1)你的一个测试这样做List&lt;Integer&gt; sortedList = new ArrayList&lt;&gt;(sourceList); - 暗示这现在是基准本身的一部分(设置方法类是为此)2)你正在洗牌集合 once - 之后所有线程/测试都相同 @lexicore(续)/试验;这可能会触发您不知道的优化。正确的方法是生成一些伪随机的东西......好吧,下一个词不容易选择。 在您的示例中它应该是Invocation,因为您正在排序,并且 IIRC 对已经排序的列表进行排序比没有排序的列表快得多(JMH 甚至有一个关于冒泡排序的示例) .因此,应该为每个已经改组和乱序的试验提供数据(在我的回答中阅读我的最后两个 cmets) @lexicore 3) 然后阅读@Fork 4) @OperationsPerInvocation 是危险的,就像您不真正理解的任何其他 JMH 注释一样 - 样本可以提供帮助! 5) 最痛苦的一个:JMH 不会告诉你你做错了什么——你要衡量“某事”,无论是否有意义都是完全不同的故事

以上是关于什么更有效:排序流或排序列表?的主要内容,如果未能解决你的问题,请参考以下文章

这是对多个链接列表进行排序的有效方法吗?

为啥这个用于排序的下拉列表在 Internet Explorer 中有效,但在 Chrome 中无效?

归并排序

搜索排序的链表时,二进制或顺序/线性搜索是不是更有效?

`sorted(list)` 与 `list.sort()` 有啥区别?

将 Scala 数组转换为唯一排序列表的有效方法