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.19秒):

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.1s也是永恒的,对吧?

更新

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

我对a、b<1000而不是100:

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

更新2sum+= c - '0';替换sum+=Integer.valueOf(c+"");(谢谢Peter!)缩短了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();

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

循环:3192
lambda:3140
lambda并行: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

您面临的问题是您正在寻找次优代码。当您有可能经过大量优化的代码时,您非常依赖JVM是否足够智能来优化您的代码。循环存在的时间要长得多,人们对它的理解也更好。

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

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

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

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

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

最新更新