【问题标题】:Is there ever a reason not to use Java 8's parallelSort?有没有理由不使用 Java 8 的 parallelSort?
【发布时间】:2019-11-12 10:40:11
【问题描述】:

我正在阅读this question,了解 Java 的 Arrays.sortArrays.parallelSort 之间的区别,这已经有几年的历史了。令我惊讶的是,只有一个问题提到了使用parallelSort 的任何缺点;也就是说,如果您使用大量 CPU,则加速会降低。

假设您不在某种专门的单线程环境中,是否应该总是选择parallelSort?有没有理由不这样做?请注意,上述问题的答案之一提到,如果元素少于 4096 个,parallelSort 无论如何都会调用sort

【问题讨论】:

  • 在 Java EE 容器中运行时可能会出现问题。
  • @JFMeier 否,Arrays.parallelSort 明确检查公共池是否只有一个线程,因此公共池中只有 1 个线程时性能不会变差(与非并行排序相比),例如,参见here。但是,是的,公共池在 servlet 上下文中只有很少 / 1 个线程,这可能是一个陷阱

标签: java sorting parallel-processing


【解决方案1】:

使用Arrays.parallelSort 有一些缺点

  • 它使用 ForkJoinPool.commonPool() 并会与默认使用它的其他函数(例如 parallel() 在流上)进行斗争
  • Arrays.parallelSort 使用的线程池不可配置(仅在全局级别通过增加公共池线程数量)
  • 它在小型数据集上表现更差(通常数组包含很少的元素,JDK 甚至承认例如大多数 ArrayList stay empty for their whole lifetime 这为不实例化永远不会的数组节省了相当多的内存和 CPU 时间填写)

还有一个轶事场景:假设您实现了一些需要排序的纸牌游戏。它非常容易并行化多个游戏执行,而不是并行化一次运行的排序机制,这可能只占用整个游戏循环的一小部分。您现在失去了一种简单的并行化方法(例如,在遗传算法的上下文中运行游戏时)。

但是,如果您碰巧有大型数组并且排序是应用程序运行时的重要组成部分,请使用Arrays.parallelSort

编辑: 即使Arrays.parallelSort 切换到正常排序,如果给定数组的元素少于 4096 个:这完全是为了表明意图——如果可能的话,你想要一个并行排序,它的含义与调用 sort 不同。并且要吹毛求疵:它确实在小型数组上表现更差,因为它必须额外检查数组是否包含少于 4096 个元素以及一些关于公共池线程数的其他检查(开销当然可以忽略不计):) .

【讨论】:

  • 您可以使用自定义 ForkJoinPool 进行流排序,如本期所示:stackoverflow.com/questions/21163108/…
  • 当然可以,我只是在谈论默认值和Arrays.parallelSort 的可能性。虽然很重要
  • @roookeee 我不同意 ArrayList 的生命周期为空 - 添加的原因是在很多情况下确实没有添加任何内容,如 this Q&A 中所述;但是Arrays 在这一点上与ArrayList 有什么关系?
  • @Eugene 这有点牵强,是的。我提到了ArrayList,因为我猜想大多数在运行时动态创建的数组(读取:未知大小)的起源是将集合转换为数组以获得更好的排序性能(或类似性能),或者当它知道没有进一步将完成加法(我仍然会争论为什么要转换为数组)。我得出这个结论/启发式,因为接收/拥有需要作为一大块排序的巨大数组(主观上)是不寻常的。我会更正我的答案以反映我的意图或删除ArrayList 部分,谢谢!
【解决方案2】:

除了常见的池使用和可优化的最小大小等原因之外,如果您通常有许多需要并行排序的事务,您可能也不需要并行化单个排序。

在这种情况下,您可以通过拆分工作包来避免开销。 (但是,具有可配置并行工作的可控执行器也适用于多线程提交 - 您只需增加停放线程和上下文切换的数量)

【讨论】:

    【解决方案3】:

    这与何时使用stream()parallelStream() 的问题没有太大区别——这取决于您拥有多少数据。当然,在并行排序 10 个元素时,大部分时间将被底层的线程框架(文档未指定)消耗,而不是由排序本身消耗。

    但您也必须想知道为什么 IMO 会引入这种方法。硬件正在(已经移动?)向 许多 CPU 移动,而不是更多 GHz,因此对于任何希望在未来 20 年内仍然存在的语言来说,并行处理只是正常的过程.

    至于您需要多少数据才能实际上parallelSort 而不是sort 上表现出色,并且知道我们需要至少 MIN_ARRAY_SORT_GRAN + 1获得任何潜在利益;编写一个适当的测试来证明对于这个特定的设置和运行,您至少需要X 数字,这并不复杂。您还必须考虑到某些数组可能已经排序(进一步解释),而有些可能完全未排序(例如5,4,3,2,1),这会给第二个数组带来一些惩罚。

    获取一些随机数据并进行测试:

    @Warmup(iterations = 10)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @Measurement(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS)
    public class ParallelSort {
    
        public static void main(String[] args) throws Exception {
            Options opt = new OptionsBuilder()
                .include(ParallelSort.class.getName())
                .build();
    
            new Runner(opt).run();
        }
    
        @Benchmark
        @BenchmarkMode(Mode.AverageTime)
        @Fork(1)
        public int[] parallel(ParallelSortExecutionPlan plan) {
            Arrays.parallelSort(plan.ints());
            return plan.ints();
        }
    
        @Benchmark
        @BenchmarkMode(Mode.AverageTime)
        @Fork(1)
        public int[] nonParallel(ParallelSortExecutionPlan plan) {
            Arrays.sort(plan.ints());
            return plan.ints();
        }
    }
    
    
    @State(Scope.Benchmark)
    public class ParallelSortExecutionPlan {
    
        @Param(value = {"10", "100", "1000", "10000", "100000", "1000000"})
        private int howMany;
    
        private int[] ints;
    
        public static void main(String[] args) {
        }
    
        @Setup(Level.Invocation)
        public void setUp() {
            ints = new int[howMany];
            for (int i = 0; i < howMany; ++i) {
                ints[i] = ThreadLocalRandom.current().nextInt();
            }
        }
    
        int[] ints() {
            return ints;
        }
    }
    

    请注意第二类使用@Setup(Level.Invocation)(如果你知道一点JMH)——这是一个非常锋利的工具;但我使用它是因为我希望该方法的每个Invocation 都有一个未排序的数组。否则,如果 Trial 将被使用 - 只有第一个调用将是未排序的数组,@Benhcmark 方法的所有其他调用将已经排序。为了好玩,您可以将单行更改为 @Setup(Level.Trial) 例如并查看结果,它们会让非常意义不大。

    运行它会显示:

    Benchmark                 (howMany)  Mode  Cnt         Score   Error  Units
    
    ParallelSort.nonParallel         10  avgt    2       128.847          ns/op
    ParallelSort.parallel            10  avgt    2       116.656          ns/op
    
    ParallelSort.nonParallel        100  avgt    2      1956.746          ns/op
    ParallelSort.parallel           100  avgt    2      1963.335          ns/op
    
    ParallelSort.nonParallel       1000  avgt    2     32162.611          ns/op
    ParallelSort.parallel          1000  avgt    2     31716.915          ns/op
    
    ParallelSort.nonParallel      10000  avgt    2    423531.663          ns/op
    ParallelSort.parallel         10000  avgt    2    201802.609          ns/op
    
    ParallelSort.nonParallel     100000  avgt    2   6503511.987          ns/op
    ParallelSort.parallel        100000  avgt    2   1363169.661          ns/op
    
    ParallelSort.nonParallel    1000000  avgt    2  69058738.586          ns/op
    ParallelSort.parallel       1000000  avgt    2  13469112.930          ns/op
    

    对我来说,这是一个非常期待的输出。

    【讨论】:

    • 漂亮的 JMH 长凳!我会用固定值播种Random。它将产生更具可比性的结果,因为并行和非并行运行在可能具有不同起始种子的单独分叉中:它们(可能)对不同的值进行排序,这可能对排序性能/时间产生重大影响,这可能会使结果产生偏差
    • @roookeee 他们确实对完全不同的数组进行排序,只是大小相同。我正在努力……
    【解决方案4】:

    不,对于足够小的数组,我会说不。设置线程的开销不会导致明显的加速。

    关键是“足够小”。不会对所有问题都给出相同的答案。

    永远不应应用教条,除非是在此教条规则的情况下。就像我们永远不能容忍的唯一事情就是不容忍一样。那里有一个波普尔悖论。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-11-10
      • 1970-01-01
      • 2013-08-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-09
      • 2016-02-05
      相关资源
      最近更新 更多