Java 8 - 外部迭代的性能优于内部迭代?

Age*_*ntX 5 java iterator java-8 java-stream

所以我正在阅读一本关于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;
}
Run Code Online (Sandbox Code Playgroud)

顺序一个 - 使用内部迭代

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;
}
Run Code Online (Sandbox Code Playgroud)

我尝试对它们进行一些基准测试,结果发现使用外部迭代的那个比使用内部迭代的那个好得多.

这是我的驱动程序代码:

public static void main(String[] args) {

    long n = 100000000L;

    for(int i=0;i<10000;i++){
    iterativeSum(n);
    sequentialSum(n);
    }
    iterativeSum(n);
    sequentialSum(n);
}
Run Code Online (Sandbox Code Playgroud)

Iteravtive的运行时间总是<50ms,而Sequential的执行时间总是> 250ms.

我无法理解为什么内部迭代不会在这里执行外部迭代?

Tag*_*eev 12

尽管所呈现的结果完全不相关,但观察到的效果实际上发生了: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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是基线测试:普通循环.我的方框上的结果如下:

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
Run Code Online (Sandbox Code Playgroud)

这是您基于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);
}
Run Code Online (Sandbox Code Playgroud)

结果如下所示:

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
Run Code Online (Sandbox Code Playgroud)

非常令人失望:慢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);
}
Run Code Online (Sandbox Code Playgroud)

结果如下:

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
Run Code Online (Sandbox Code Playgroud)

现在好多了,但仍然慢了2.8-10倍.另一种方法是使用范围:

public static long rangeSum(long n) {
    return LongStream.rangeClosed(1, n).sum();
}

@Benchmark
public long rs() {
    return rangeSum(n);
}
Run Code Online (Sandbox Code Playgroud)

结果如下:

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
Run Code Online (Sandbox Code Playgroud)

现在它慢了3.1-4.5倍.这种缓慢的原因是Stream API具有非常长的调用链,该调用链达到MaxInlineLevelJVM限制,因此默认情况下无法完全内联.您可以增加此限制设置,-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
Run Code Online (Sandbox Code Playgroud)

好多了:现在它只慢了1.05-1.5倍.

这个测试的问题在于迭代版本的循环体非常简单,因此可以通过JIT编译器有效地展开和矢量化,并且对于复杂的Stream API代码,以相同的效率执行此操作要困难得多.但是在实际应用中,你不可能在循环中对连续数字求和(为什么不写n*(n+1)/2呢?).使用实际问题即使使用默认MaxInlineLevel设置,Stream API开销也会低得多.