所以我正在阅读一本关于Java 8的书,当我看到他们在外部和内部迭代之间进行比较并考虑比较两者时,性能明智。
我有一个方法,它只是将一个整数序列汇总到n
。
迭代的:
private static long iterativeSum(long n) {
long startTime = System.nanoTime();
long sum = 0;
for(long i=1; i<=n; i++) {
sum+=i;
}
long endTime = System.nanoTime();
System.out.println("Iterative Sum Duration: " + (endTime-startTime)/1000000);
return sum;
}
顺序一个 - 使用内部迭代
private static long sequentialSum(long n) {
long startTime = System.nanoTime();
//long sum = LongStream.rangeClosed(1L, n)
long sum = Stream.iterate(1L, i -> i+1)
.limit(n)
.reduce(0L, (i,j) -> i+j);
long endTime = System.nanoTime();
System.out.println("Sequential Sum Duration: " + (endTime-startTime)/1000000);
return sum;
}
我尝试对它们进行一些基准测试,结果发现使用外部迭代的那个比使用内部迭代的那个好得多。
这是我的驱动程序代码:
public static void main(String[] args) {
long n = 100000000L;
for(int i=0;i<10000;i++){
iterativeSum(n);
sequentialSum(n);
}
iterativeSum(n);
sequentialSum(n);
}
Iteravtive的运行时间始终是&lt; 50ms,而Sequential one的执行时间总是> 250毫秒。
我无法理解为什么内部迭代没有在这里执行外部迭代?
答案 0 :(得分:11)
即使所呈现的结果完全无关,但实际观察到的效果仍然存在:Stream API确实存在开销,即使在预热之后,这些简单任务也无法在实际应用中完全消除。让我们写一个JMH基准:
@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(3)
@State(Scope.Benchmark)
public class IterativeSum {
@Param({ "100", "10000", "1000000" })
private int n;
public static long iterativeSum(long n) {
long sum = 0;
for(long i=1; i<=n; i++) {
sum+=i;
}
return sum;
}
@Benchmark
public long is() {
return iterativeSum(n);
}
}
这里的基线测试:普通循环。我的方框上的结果如下:
Benchmark (n) Mode Cnt Score Error Units
IterativeSum.is 100 avgt 30 0.074 ± 0.001 us/op
IterativeSum.is 10000 avgt 30 6.361 ± 0.009 us/op
IterativeSum.is 1000000 avgt 30 688.527 ± 0.910 us/op
这是您基于Stream API的迭代版本:
public static long sequentialSumBoxed(long n) {
return Stream.iterate(1L, i -> i+1).limit(n)
.reduce(0L, (i,j) -> i+j);
}
@Benchmark
public long ssb() {
return sequentialSumBoxed(n);
}
结果如下:
Benchmark (n) Mode Cnt Score Error Units
IterativeSum.ssb 100 avgt 30 1.253 ± 0.084 us/op
IterativeSum.ssb 10000 avgt 30 134.959 ± 0.421 us/op
IterativeSum.ssb 1000000 avgt 30 9119.422 ± 22.817 us/op
非常令人失望:慢了13-21倍。这个版本里面有很多装箱操作,这就是创建原始流专业化的原因。我们来检查非盒装版本:
public static long sequentialSum(long n) {
return LongStream.iterate(1L, i -> i+1).limit(n)
.reduce(0L, (i,j) -> i+j);
}
@Benchmark
public long ss() {
return sequentialSum(n);
}
结果如下:
Benchmark (n) Mode Cnt Score Error Units
IterativeSum.ss 100 avgt 30 0.661 ± 0.001 us/op
IterativeSum.ss 10000 avgt 30 67.498 ± 5.732 us/op
IterativeSum.ss 1000000 avgt 30 1982.687 ± 38.501 us/op
现在好多了,但仍然慢了2.8-10倍。另一种方法是使用范围:
public static long rangeSum(long n) {
return LongStream.rangeClosed(1, n).sum();
}
@Benchmark
public long rs() {
return rangeSum(n);
}
结果如下:
Benchmark (n) Mode Cnt Score Error Units
IterativeSum.rs 100 avgt 30 0.316 ± 0.001 us/op
IterativeSum.rs 10000 avgt 30 28.646 ± 0.065 us/op
IterativeSum.rs 1000000 avgt 30 2158.962 ± 514.780 us/op
现在它慢了3.1-4.5倍。这种缓慢的原因是Stream API具有非常长的调用链,该调用链达到MaxInlineLevel
JVM限制,因此默认情况下无法完全内联。您可以像-XX:MaxInlineLevel=20
一样增加此限制设置,并获得以下结果:
Benchmark (n) Mode Cnt Score Error Units
IterativeSum.rs 100 avgt 30 0.111 ± 0.001 us/op
IterativeSum.rs 10000 avgt 30 9.552 ± 0.017 us/op
IterativeSum.rs 1000000 avgt 30 729.935 ± 31.915 us/op
好多了:现在它只慢了1.05-1.5倍。
这个测试的问题在于迭代版本的循环体非常简单,因此可以通过JIT编译器有效地展开和矢量化,并且对于复杂的Stream来说,以相同的效率执行此操作要困难得多API代码。但是在实际应用中,您不太可能在循环中对连续数字求和(为什么不写n*(n+1)/2
?)。使用实际问题即使使用默认MaxInlineLevel
设置,流API开销也会低得多。