【问题标题】:Java 8 - External Iteration performing better than Internal Iteration?Java 8 - 外部迭代性能优于内部迭代?
【发布时间】:2016-02-08 17:19:22
【问题描述】:

所以当我看到他们对外部迭代和内部迭代进行比较时,我正在阅读一本关于 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 的运行时间总是 250ms。

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

【问题讨论】:

  • fyi stackoverflow.com/questions/504103/… 您的测试没有按照您的预期进行
  • 我尝试在循环中运行这些方法超过一千次,以预热 JIT 并遍历所有代码路径。但我没有一次看到 2 之间的差距缩小。迭代总是 250ms

标签: java iterator java-8 java-stream


【解决方案1】:

尽管呈现的结果完全不相关,但观察到的效果确实发生了: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 设置,Stream API 开销也会低得多。

【讨论】:

  • 看起来很有趣,我会更多地研究 MaxInlineLevel 做更多的测试,然后接受你的答案。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-18
  • 2016-03-13
相关资源
最近更新 更多