为什么使用for循环的倒数之和比流快400倍?

Vip*_*pin 8 java performance java-8 java-stream jmh

这段代码基于3种不同的方法来计算a元素的倒数之和double[].

  1. a for-loop
  2. Java 8流
  3. colt数学库

使用简单for循环的计算比使用流的计算速度快约400倍的原因是什么?(或者基准测试代码中是否有任何需要改进的地方?或者使用流来计算更快的方法?)

代码:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import cern.colt.list.DoubleArrayList;
import cern.jet.stat.Descriptive;
import org.openjdk.jmh.annotations.*;

@State(Scope.Thread)
public class MyBenchmark {

    public static double[] array;

    static {
        int num_of_elements = 100;
        array = new double[num_of_elements];
        for (int i = 0; i < num_of_elements; i++) {
            array[i] = i+1;
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public void testInversionSumForLoop(){
        double result = 0;
        for (int i = 0; i < array.length; i++) {
            result += 1.0/array[i];
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public void testInversionSumUsingStreams(){
        double result = 0;
        result = Arrays.stream(array).map(d -> 1/d).sum();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public void testInversionSumUsingCernColt(){
        double result = Descriptive.sumOfInversions(new DoubleArrayList(array), 0, array.length-1);
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

/**
 * Results
 * Benchmark                                  Mode  Cnt    Score    Error  Units
 * MyBenchmark.testInversionSumForLoop        avgt  200    1.647 ±  0.155  ns/op
 * MyBenchmark.testInversionSumUsingCernColt  avgt  200  603.254 ± 22.199  ns/op
 * MyBenchmark.testInversionSumUsingStreams   avgt  200  645.895 ± 20.833  ns/o
 */
Run Code Online (Sandbox Code Playgroud)

更新:这些结果显示Blackhome.consume或return是必要的,以避免jvm优化.

/**
 * Updated results after adding Blackhole.consume
 * Benchmark                                  Mode  Cnt    Score    Error  Units
 * MyBenchmark.testInversionSumForLoop        avgt  200  525.498 ± 10.458  ns/op
 * MyBenchmark.testInversionSumUsingCernColt  avgt  200  517.930 ±  2.080  ns/op
 * MyBenchmark.testInversionSumUsingStreams   avgt  200  582.103 ±  3.261  ns/op
 */
Run Code Online (Sandbox Code Playgroud)

oracle jdk版本"1.8.0_181",Darwin内核版本17.7.0

Kar*_*cki 11

在您的示例中,JVM很可能完全优化循环,因为result计算后永远不会读取值.您应该使用Blackhole消耗result象下面这样:

@State(Scope.Thread)
@Warmup(iterations = 10, time = 200, timeUnit = MILLISECONDS)
@Measurement(iterations = 20, time = 500, timeUnit = MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {

  static double[] array;

  static {
    int num_of_elements = 100;
    array = new double[num_of_elements];
    for (int i = 0; i < num_of_elements; i++) {
      array[i] = i + 1;
    }
  }

  double result = 0;

  @Benchmark
  public void baseline(Blackhole blackhole) {
    result = 1;
    result = result / 1.0;
    blackhole.consume(result);
  }

  @Benchmark
  public void testInversionSumForLoop(Blackhole blackhole) {
    for (int i = 0; i < array.length; i++) {
      result += 1.0 / array[i];
    }
    blackhole.consume(result);
  }

  @Benchmark
  public void testInversionSumUsingStreams(Blackhole blackhole) {
    result = Arrays.stream(array).map(d -> 1 / d).sum();
    blackhole.consume(result);
  }

}
Run Code Online (Sandbox Code Playgroud)

这个新基准显示出预期的4倍差异.循环受益于JVM中的许多优化,并且不涉及像流一样创建新对象.

Benchmark                                 Mode  Cnt    Score   Error  Units
MyBenchmark.baseline                      avgt  100    2.437 ±  0.139  ns/op
MyBenchmark.testInversionSumForLoop       avgt  100  135.512 ± 13.080  ns/op
MyBenchmark.testInversionSumUsingStreams  avgt  100  506.479 ±  4.209  ns/o
Run Code Online (Sandbox Code Playgroud)

我试图添加一个基线来显示我的机器上单个操作的成本.基线ns/ops类似于您的循环ns/ops,IMO确认您的循环已经过优化.

我希望有人告诉我这个基准测试场景的基线是什么.

我的环境:

openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)

Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
Linux 4.15.0-43-generic #46-Ubuntu SMP Thu Dec 6 14:45:28 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
Run Code Online (Sandbox Code Playgroud)

  • 这个问题是[这个问题](/sf/ask/3790087731/)的后续问题,并且如[评论]中所述(/sf/ask/3790087731/ -sum-is-not-correct-using-stream-reduce#comment95120909_54144110),`DoubleStream.sum()`使用[Kahan求和算法](https://en.wikipedia.org/wiki/Kahan_summation_algorithm),它提供了更高的精度,但也比普通的循环需要更多的时间.所以它不仅仅是循环与流.您可以将`.sum()`与`reduce(0,Double :: sum)`进行比较...... (5认同)
  • @KarolDowbecki,您可以确定您的基线操作中已删除了该部分。对于您的原始版本,“结果+ = 1.0 / 1.0;”,术语“ 1.0 / 1.0”是编译时常量。对于更改后的变体,结果= 1;result = result / 1.0;`,运行时优化器必须加入,但随后不会被两个后续(冗余)分配所混淆。因此,您将增量操作更改为简单的分配“ 1”,这就是为什么它变得更快的原因。真正的分裂将大大减慢速度。您应该尝试类似`result =(result + 1)/ 1.0;`之类的东西。 (2认同)