为了练习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秒(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();
我的问题
我知道microbenchmarks是脆弱的,并行性只对大问题是值得的,但对于CPU来说,甚至0.1s都是永恒的,对吗?
更新
我使用Eclipse Kepler中的Junit 4框架进行测量(它显示了执行测试所需的时间)。
我的结果为a,b <1000而不是100:
更新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();
答案 0 :(得分:22)
我根据您的代码创建了一个快速而肮脏的微基准测试。结果是:
循环:3192
lambda:3140
lambda parallel: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
答案 1 :(得分:3)
您遇到的问题是您正在寻找次优代码。当您拥有可能经过大量优化的代码时,您非常依赖于JVM是否足够智能来优化代码。循环已经存在很长时间并且更好理解。
你的循环代码有一个很大的不同,就是你的工作集非常小。您一次只考虑一个最大数字总和。这意味着代码是缓存友好的,并且您拥有非常短暂的对象。在stream()情况下,您正在构建集合,在任何时候工作集中都有更多集合,使用更多缓存,并且开销更大。我希望您的GC时间更长和/或更频繁。
为什么流变量比旧变量慢得多?
在开发Java之前,循环已经很好地优化了。它们可以非常有效地映射到硬件。流是相当新的,并没有大幅优化。
为什么parallel()语句实际上将时间从0.19s增加到0.25s?
您很可能在共享资源上有瓶颈。你创造了相当多的垃圾,但这通常是相当并发的。使用更多线程,只能保证你会有更多的开销,但它并不能确保你可以利用你拥有的额外CPU能力。