【问题标题】:Proper usage of parallel streams in Java在 Java 中正确使用并行流
【发布时间】:2019-02-24 07:13:34
【问题描述】:

我正在用 Java 中的并行流进行试验,为此我有以下代码用于计算 n 之前的素数。

基本上我有两种方法

  • calNumberOfPrimes(long n) - 4 种不同的变体
  • isPrime(long n) - 2 种不同的变体

实际上,我对上述每种方法都有 2 种不同的变体,一种使用并行流的变体,另一种不使用并行流的变体。

    // itself uses parallel stream and calls parallel variant isPrime
    private static long calNumberOfPrimesPP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .parallel()
                .filter(i -> isPrimeParallel(i))
                .count();
    }

    // itself uses parallel stream and calls non-parallel variant isPrime
    private static long calNumberOfPrimesPNP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .parallel()
                .filter(i -> isPrimeNonParallel(i))
                .count();
    }

    // itself uses non-parallel stream and calls parallel variant isPrime
    private static long calNumberOfPrimesNPP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .filter(i -> isPrimeParallel(i))
                .count();
    }

    // itself uses non-parallel stream and calls non-parallel variant isPrime
    private static long calNumberOfPrimesNPNP(long n) {
        return LongStream
                .rangeClosed(2, n)
                .filter(i -> isPrimeNonParallel(i))
                .count();
    }
    // uses parallel stream
    private static boolean isPrimeParallel(long n) {
        return LongStream
                .rangeClosed(2, (long) Math.sqrt(n))
                .parallel()
                .noneMatch(i -> n % i == 0);
    }

    // uses non-parallel stream
    private static boolean isPrimeNonParallel(long n) {
        return LongStream
                .rangeClosed(2, (long) Math.sqrt(n))
                .noneMatch(i -> n % i == 0);
    }

我试图找出在正确使用并行流和效率方面,calNumberOfPrimesPPcalNumberOfPrimesPNPcalNumberOfPrimesNPPcalNumberOfPrimesNPNP 中哪一个是最好的,以及为什么它是最好的。

我尝试将所有这 4 种方法计时 50 次,并使用以下代码取平均值:

    public static void main(String[] args) throws Exception {
        int iterations = 50;
        int n = 1000000;
        double pp, pnp, npp, npnp;
        pp = pnp = npp = npnp = 0;
        for (int i = 0; i < iterations; i++) {
            Callable<Long> runner1 = () -> calNumberOfPrimesPP(n);
            Callable<Long> runner2 = () -> calNumberOfPrimesPNP(n);
            Callable<Long> runner3 = () -> calNumberOfPrimesNPP(n);
            Callable<Long> runner4 = () -> calNumberOfPrimesNPNP(n);

            pp += TimeIt.timeIt(runner1);
            pnp += TimeIt.timeIt(runner2);
            npp += TimeIt.timeIt(runner3);
            npnp += TimeIt.timeIt(runner4);
        }
        System.out.println("___________final results___________");
        System.out.println("avg PP = " + pp / iterations);
        System.out.println("avg PNP = " + pnp / iterations);
        System.out.println("avg NPP = " + npp / iterations);
        System.out.println("avg NPNP = " + npnp / iterations);
    }

TimeIt.timeIt 只返回以毫秒为单位的执行时间。我得到以下输出:

___________final results___________
avg PP = 2364.51336366
avg PNP = 265.27284506
avg NPP = 11424.194316620002
avg NPNP = 1138.15516624

现在我试图推断上述执行时间:

  • PP 变体不如PNP 变体快,因为所有并行流都使用公共 fork-join 线程池,如果我们提交一个长时间运行的任务,我们实际上会阻塞池中的所有线程。
  • 但上述参数也适用于NPP 变体,因此NPP 变体也应该与PNP 变体差不多快。 (但事实并非如此,NPP 变体在耗时方面是最差的)。有人可以解释一下这背后的原因吗?

我的问题:

  • 对于 PNP 变体的小运行时间,我的推理是否正确?
  • 我错过了什么吗?
  • 为什么NPP 变体是最差的(就运行时间而言)?

TimeIt 如何测量时间:

class TimeIt {
    private TimeIt() {
    }

    /**
     * returns the time to execute the Callable in milliseconds
     */
    public static <T> double timeIt(Callable<T> callable) throws Exception {
        long start = System.nanoTime();
        System.out.println(callable.call());
        return (System.nanoTime() - start) / 1.0e6;
    }
}

PS:我知道这不是计算素数的最佳方法。 Sieve of Eratosthenes 和其他更复杂的方法可以做到这一点。但是通过这个例子,我只想了解并行流的行为以及何时使用它们。

【问题讨论】:

  • 如果你做微基准测试,你应该使用JMH。即使您不使用 JMH,也请始终为 HotspotVM 进行一些老化迭代以发挥其黑色 JIT 魔法。
  • @Turing85 是的,我理解 JMH 的重要性。但是对于这个简单的案例,我认为我想更多地关注流在幕后所做的事情以及如何正确使用并行流。 JMH 是我的下一步 :)
  • JMH 应该永远是第一步。正如我所说:HotspotVM 会做不可预测的事情。如果没有适当的老化,您的性能测量就没有明显的价值。
  • 你的测试有很多缺陷,这种设置的问题是你可能对现实有错误的印象,这意味着你得到的那些数字可能是如此不真实,以至于最好根本不看它们。
  • 你是在说这个——作为一个初学者,这不是你开始理解这一点的地方。如果您真的想了解哪种方法更快以及为什么 - 我会从 JMH 示例开始,这是我的建议。

标签: java java-8 parallel-processing java-stream forkjoinpool


【解决方案1】:

我想,很明显,为什么 NPP 这么慢。

将结果数字排列在表格中:

       |    _P    |   _NP
-------+----------+---------
  P_   |   2364   |   265
-------+----------+---------
  NP_  |  11424   |  1138
-------+----------+---------

所以你看到当外部流是并行的时候它总是更快。这是因为流中有很多工作要做。因此,与要完成的工作相比,处理并行流的额外开销较低。

您还看到,当内部流不并行时,它总是更快。 isPrimeNonParallelisPrimeParallel 快。这是因为流中没有太多工作要做。在大多数情况下,经过几步之后就可以清楚地知道该数字不是素数。一半的数字是偶数(只有一步)。与要完成的工作相比,处理并行流的额外开销很高。

【讨论】:

  • 您的回答很好地说明了这一点。实际上它应该考虑并行流的开销。如果您可以相当快地获得结果,那么并行流可能不是一个好的选择。感谢你的回答。我认为它钉了它。 :) 我认为我关于并行流的通用 fork-join 池的推理是正确的,但它不是这里的影响因素。
  • @LavishKothari 是的,我想是的。如果外部流中已经有“足够”的线程,它也不会给你更多的并行性。
猜你喜欢
  • 2021-05-25
  • 2016-08-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多