我在解决Problem 205的Project Euler时发现了这一点。问题如下:
彼得有九个四面(金字塔)骰子,每个骰子都有编号为1,2,3,4的面孔。 科林有六个六面(立方体)骰子,每个骰子都有编号为1,2,3,4,5,6的面孔。 彼得和科林掷骰子并比较总数:总得分最高。如果总数相等,结果就是平局。金字塔皮特击败立方科林的几率是多少?将您的答案四舍五入到0.abcdefg
形式的七位小数
我用Guava写了一个天真的解决方案:
import com.google.common.collect.Sets;
import com.google.common.collect.ImmutableSet;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
public class Problem205 {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
List<Integer> peter = Sets.cartesianProduct(Collections.nCopies(9, ImmutableSet.of(1, 2, 3, 4)))
.stream()
.map(l -> l
.stream()
.mapToInt(Integer::intValue)
.sum())
.collect(Collectors.toList());
List<Integer> colin = Sets.cartesianProduct(Collections.nCopies(6, ImmutableSet.of(1, 2, 3, 4, 5, 6)))
.stream()
.map(l -> l
.stream()
.mapToInt(Integer::intValue)
.sum())
.collect(Collectors.toList());
long startTime2 = System.currentTimeMillis();
// IMPORTANT BIT HERE! v
long solutions = peter
.stream()
.mapToLong(p -> colin
.stream()
.filter(c -> p > c)
.count())
.sum();
// IMPORTANT BIT HERE! ^
System.out.println("Counting solutions took " + (System.currentTimeMillis() - startTime2) + "ms");
System.out.println("Solution: " + BigDecimal
.valueOf(solutions)
.divide(BigDecimal
.valueOf((long) Math.pow(4, 9) * (long) Math.pow(6, 6)),
7,
RoundingMode.HALF_UP));
System.out.println("Found in: " + (System.currentTimeMillis() - startTime) + "ms");
}
}
我突出显示的代码使用简单的filter()
,count()
和sum()
,在Java 9中的运行速度似乎比Java 8快得多。具体来说,Java 8计算解决方案在我的机器上37465ms。 Java 9大约需要16000毫秒,无论是运行使用Java 8编译的文件还是使用Java 9编译的文件都是一样的。
如果我将流代码替换为看似确切的预流等效代码:
long solutions = 0;
for (Integer p : peter) {
long count = 0;
for (Integer c : colin) {
if (p > c) {
count++;
}
}
solutions += count;
}
它在大约35000ms内计算解决方案,Java 8和Java 9之间没有可测量的差异。
我在这里缺少什么?为什么Java 9中的代码编码速度要快得多,为什么不是for
循环?
我正在运行Ubuntu 16.04 LTS 64位。我的Java 8版本:
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
我的Java 9版本:
java version "9"
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
答案 0 :(得分:14)
Stream.count()
实现是rather dumb:它只是遍历整个流,为每个元素添加1L
。
这在JDK 9中是fixed。尽管错误报告说明了SIZED流,但新代码也改进了非大小的流。
如果用Java 8样式的实现.count()
替换.mapToLong(e -> 1L).sum()
,即使在JDK 9上它也会再次变慢。
当您将所有代码放在main
方法中时,它无法有效地进行JIT编译。此方法仅执行一次,它在解释器中开始运行,之后,当JVM检测到热循环时,它从解释模式切换到移动编译。这称为堆栈替换(OSR)。
OSR编译通常不像常规编译方法那样优化。我之前已详细解释过,请参阅this和this回答。
如果将内部循环放在单独的方法中,JIT将生成更好的代码:
long solutions = 0;
for (Integer p : peter) {
solutions += countLargerThan(colin, p);
}
...
private static int countLargerThan(List<Integer> colin, int p) {
int count = 0;
for (Integer c : colin) {
if (p > c) {
count++;
}
}
return count;
}
在这种情况下,countLargerThan
方法将正常编译,并且性能将优于JDK 8和JDK 9上的流。