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

时间:2015-11-07 17:44:47

标签: 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;
}

顺序一个 - 使用内部迭代

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毫秒。

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

1 个答案:

答案 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开销也会低得多。