Java 8嵌套循环,包含流和性能

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

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秒(没有parallel() 0.19s):

 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.8s(是的,它增加了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(); 

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

循环:3192
lambda:3140
lambda parallel:868

因此,loop和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 capSO21968918.solve avgt 32367.592 us/op capSO21968918.solveLambda avgt 31423.123 us/op capSO21968918.solveLambdaParallel avgt 8125.600 us/op 

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

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

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

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

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

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