13

为了练习 Java 8 流,我尝试将以下嵌套循环转换为 Java 8 流 API。它计算 a^b (a,b < 100) 的最大数字总和,并在我的 Core i5 760 上花费约 0.135 秒。

public static int digitSum(BigInteger x)
{
    int sum = 0;
    for(char c: x.toString().toCharArray()) {sum+=Integer.valueOf(c+"");}
    return sum;
}

@Test public void solve()
    {
        int max = 0;
        for(int i=1;i<100;i++)
            for(int j=1;j<100;j++)
                max = Math.max(max,digitSum(BigInteger.valueOf(i).pow(j)));
        System.out.println(max);
    }

我的解决方案,由于并行性,我预计会更快,实际上需要 0.25 秒(没有 0.19 秒parallel()):

int max =   IntStream.range(1,100).parallel()
            .map(i -> IntStream.range(1, 100)
            .map(j->digitSum(BigInteger.valueOf(i).pow(j)))
            .max().getAsInt()).max().getAsInt();

我的问题

  • 我做了正确的转换还是有更好的方法将嵌套循环转换为流计算?
  • 为什么流变体比旧变体慢得多?
  • 为什么parallel()语句实际上将时间从0.19s增加到0.25s?

我知道微基准是脆弱的,并行性只对大问题值得,但对于 CPU,即使是 0.1 秒也是永恒的,对吧?

更新

我使用 Eclipse Kepler 中的 Junit 4 框架进行测量(它显示了执行测试所花费的时间)。

我的 a,b<1000 而不是 100 的结果:

  • 传统循环 186s
  • 顺序流 193s
  • 并行流 55s

更新 2 替换sum+=Integer.valueOf(c+"");sum+= c - '0';(感谢彼得!)将并行方法缩短了 10 秒,使其达到 45 秒。没想到性能影响这么大!

此外,将并行度减少到 CPU 内核的数量(在我的情况下为 4 个)并没有起到太大作用,因为它只将时间减少到 44.8 秒(是的,它增加了 a 和 b=0,但我认为这不会影响性能很多):

int max = IntStream.range(0, 3).parallel().
          .map(m -> IntStream.range(0,250)
          .map(i -> IntStream.range(1, 1000)
          .map(j->.digitSum(BigInteger.valueOf(250*m+i).pow(j)))
          .max().getAsInt()).max().getAsInt()).max().getAsInt();
4

2 回答 2

23

我根据您的代码创建了一个快速而肮脏的微基准测试。结果是:

循环:3192
λ:3140
λ 并行:868

所以循环和 lambda 是等价的,并行流显着提高了性能。由于您的基准测试方法,我怀疑您的结果不可靠。

public static void main(String[] args) {
    int sum = 0;

    //warmup
    for (int i = 0; i < 100; i++) {
        solve();
        solveLambda();
        solveLambdaParallel();
    }

    {
        long start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            sum += solve();
        }
        long end = System.nanoTime();
        System.out.println("loop: " + (end - start) / 1_000_000);
    }
    {
        long start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            sum += solveLambda();
        }
        long end = System.nanoTime();
        System.out.println("lambda: " + (end - start) / 1_000_000);
    }
    {
        long start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            sum += solveLambdaParallel();
        }
        long end = System.nanoTime();
        System.out.println("lambda parallel : " + (end - start) / 1_000_000);
    }
    System.out.println(sum);
}

public static int digitSum(BigInteger x) {
    int sum = 0;
    for (char c : x.toString().toCharArray()) {
        sum += Integer.valueOf(c + "");
    }
    return sum;
}

public static int solve() {
    int max = 0;
    for (int i = 1; i < 100; i++) {
        for (int j = 1; j < 100; j++) {
            max = Math.max(max, digitSum(BigInteger.valueOf(i).pow(j)));
        }
    }
    return max;
}

public static int solveLambda() {
    return  IntStream.range(1, 100)
            .map(i -> IntStream.range(1, 100).map(j -> digitSum(BigInteger.valueOf(i).pow(j))).max().getAsInt())
            .max().getAsInt();
}

public static int solveLambdaParallel() {
    return  IntStream.range(1, 100)
            .parallel()
            .map(i -> IntStream.range(1, 100).map(j -> digitSum(BigInteger.valueOf(i).pow(j))).max().getAsInt())
            .max().getAsInt();
}

我也用 jmh 运行它,它比手动测试更可靠。结果与上述一致(每次调用微秒):

Benchmark                                Mode   Mean        Units
c.a.p.SO21968918.solve                   avgt   32367.592   us/op
c.a.p.SO21968918.solveLambda             avgt   31423.123   us/op
c.a.p.SO21968918.solveLambdaParallel     avgt   8125.600    us/op
于 2014-02-23T13:50:43.077 回答
3

您遇到的问题是您正在查看次优代码。当您的代码可能会被大量优化时,您非常依赖 JVM 是否足够聪明来优化您的代码。循环的存在时间更长,并且更好理解。

循环代码的一大区别是您的工作集非常小。您一次只考虑一个最大数字总和。这意味着代码是缓存友好的,并且您的对象寿命很短。在 stream() 情况下,您正在构建集合,在任何时候都在工作集中有更多的集合,使用更多的缓存,有更多的开销。我希望您的 GC 时间也更长和/或更频繁。

为什么流变体比旧变体慢得多?

循环在 Java 开发之前就已经得到了很好的优化。它们可以非常有效地映射到硬件。流是相当新的,并且没有经过大量优化。

为什么parallel()语句实际上将时间从0.19s增加到0.25s?

您很可能在共享资源上遇到瓶颈。你创造了相当多的垃圾,但这通常是相当并发的。使用更多线程,只能保证您将拥有更多开销,并不能确保您可以利用您拥有的额外 CPU 能力。

于 2014-02-23T13:45:17.320 回答