什么时候应该优先使用流而不是传统循环以获得最佳性能?流是否利用了分支预测?

When should streams be preferred over traditional loops for best performance? Do streams take advantage of branch-prediction?

我刚刚阅读了有关 Branch-Prediction 的内容,想尝试一下它如何与 Java 8 Streams 一起使用。

然而,Streams 的性能总是比传统循环差。

int totalSize = 32768;
int filterValue = 1280;
int[] array = new int[totalSize];
Random rnd = new Random(0);
int loopCount = 10000;

for (int i = 0; i < totalSize; i++) {
    // array[i] = rnd.nextInt() % 2560; // Unsorted Data
    array[i] = i; // Sorted Data
}

long start = System.nanoTime();
long sum = 0;
for (int j = 0; j < loopCount; j++) {
    for (int c = 0; c < totalSize; ++c) {
        sum += array[c] >= filterValue ? array[c] : 0;
    }
}
long total = System.nanoTime() - start;
System.out.printf("Conditional Operator Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    for (int c = 0; c < totalSize; ++c) {
        if (array[c] >= filterValue) {
            sum += array[c];
        }
    }
}
total = System.nanoTime() - start;
System.out.printf("Branch Statement Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    sum += Arrays.stream(array).filter(value -> value >= filterValue).sum();
}
total = System.nanoTime() - start;
System.out.printf("Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    sum += Arrays.stream(array).parallel().filter(value -> value >= filterValue).sum();
}
total = System.nanoTime() - start;
System.out.printf("Parallel Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

输出:

  1. 对于排序数组:

    Conditional Operator Time : 294062652 ns, (0.294063 sec) 
    Branch Statement Time : 272992442 ns, (0.272992 sec) 
    Streams Time : 806579913 ns, (0.806580 sec) 
    Parallel Streams Time : 2316150852 ns, (2.316151 sec) 
    
  2. 对于未排序的数组:

    Conditional Operator Time : 367304250 ns, (0.367304 sec) 
    Branch Statement Time : 906073542 ns, (0.906074 sec) 
    Streams Time : 1268648265 ns, (1.268648 sec) 
    Parallel Streams Time : 2420482313 ns, (2.420482 sec) 
    

我使用 List:
尝试了相同的代码 list.stream() 而不是 Arrays.stream(array)
list.get(c) 而不是 array[c]

输出:

  1. 对于排序列表:

    Conditional Operator Time : 860514446 ns, (0.860514 sec) 
    Branch Statement Time : 663458668 ns, (0.663459 sec) 
    Streams Time : 2085657481 ns, (2.085657 sec) 
    Parallel Streams Time : 5026680680 ns, (5.026681 sec) 
    
  2. 对于未排序的列表

    Conditional Operator Time : 704120976 ns, (0.704121 sec) 
    Branch Statement Time : 1327838248 ns, (1.327838 sec) 
    Streams Time : 1857880764 ns, (1.857881 sec) 
    Parallel Streams Time : 2504468688 ns, (2.504469 sec) 
    

我参考了一些博客 this & this,它们提出了相同的性能问题 w.r.t 流。

  1. 我同意这样的观点,即在某些情况下使用流进行编程既好又容易,但是当我们失去性能时,为什么我们需要使用它们?我错过了什么吗?
  2. 流执行等于循环的场景是什么?是否只有在你定义的函数耗费大量时间,导致循环性能可以忽略不计的情况下?
  3. 在场景的 none 中,我可以看到流利用 分支预测 (我尝试使用排序和无序流,但没有用。它给了与普通流相比,性能影响增加一倍以上)?

I agree to the point that programming with streams is nice and easier for some scenarios but when we're losing out on performance, why do we need to use them?

性能很少成为问题。通常需要将 10% 的流重写为循环才能获得所需的性能。

Is there something I'm missing out on?

使用 parallelStream() 使用流更容易,而且可能更高效,因为很难编写高效的并发代码。

Which is the scenario in which streams perform equal to loops? Is it only in the case where your function defined takes a lot of time, resulting in a negligible loop performance?

您的基准测试存在缺陷,因为代码在启动时尚未编译。我会像 JMH 一样在循环中进行整个测试,或者我会使用 JMH。

In none of the scenario's I could see streams taking advantage of branch-prediction

分支预测是一项 CPU 功能,而不是 JVM 或流功能。

Java 是一种高级语言,使程序员无需考虑低级性能优化。

永远不要出于性能原因选择某种方法,除非您已证明这在您的实际应用程序中存在问题。

您的测量显示对流有一些负面影响,但差异低于可观察性。因此,这不是问题。 此外,此测试是 "synthetic" 情况,代码在重型生产环境中的行为可能完全不同。 此外,JIT 从您的 Java(字节)代码创建的机器代码可能会在未来的 Java(维护)版本中发生变化,并使您的测量过时。

结论:选择最能表达您(程序员)意图的语法或方法.在整个程序中保持相同的方法或语法,除非您有充分的理由进行更改。

一切都说了,但我想向您展示您的代码应该如何使用 JMH

@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@Measurement(iterations = 10, timeUnit = TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Threads(1)
@Warmup(iterations = 5, timeUnit = TimeUnit.NANOSECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {

  private final int totalSize = 32_768;
  private final int filterValue = 1_280;
  private final int loopCount = 10_000;
  // private Random rnd;

  private int[] array;

  @Setup
  public void setup() {
    array = IntStream.range(0, totalSize).toArray();

    // rnd = new Random(0);
    // array = rnd.ints(totalSize).map(i -> i % 2560).toArray();
  }

  @Benchmark
  public long conditionalOperatorTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      for (int c = 0; c < totalSize; ++c) {
        sum += array[c] >= filterValue ? array[c] : 0;
      }
    }
    return sum;
  }

  @Benchmark
  public long branchStatementTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      for (int c = 0; c < totalSize; ++c) {
        if (array[c] >= filterValue) {
          sum += array[c];
        }
      }
    }
    return sum;
  }

  @Benchmark
  public long streamsTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      sum += IntStream.of(array).filter(value -> value >= filterValue).sum();
    }
    return sum;
  }

  @Benchmark
  public long parallelStreamsTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      sum += IntStream.of(array).parallel().filter(value -> value >= filterValue).sum();
    }
    return sum;
  }
}

排序数组的结果:

Benchmark                            Mode  Cnt           Score           Error  Units
MyBenchmark.branchStatementTime      avgt   30   119833793,881 ±   1345228,723  ns/op
MyBenchmark.conditionalOperatorTime  avgt   30   118146194,368 ±   1748693,962  ns/op
MyBenchmark.parallelStreamsTime      avgt   30   499436897,422 ±   7344346,333  ns/op
MyBenchmark.streamsTime              avgt   30  1126768177,407 ± 198712604,716  ns/op

未排序数据的结果:

Benchmark                            Mode  Cnt           Score           Error  Units
MyBenchmark.branchStatementTime      avgt   30   534932594,083 ±   3622551,550  ns/op
MyBenchmark.conditionalOperatorTime  avgt   30   530641033,317 ±   8849037,036  ns/op
MyBenchmark.parallelStreamsTime      avgt   30   489184423,406 ±   5716369,132  ns/op
MyBenchmark.streamsTime              avgt   30  1232020250,900 ± 185772971,366  ns/op

我只能说JVM优化的可能性有很多,可能还涉及到branch-prediction。现在由您来解释基准测试结果。

我会在这里添加我的 0.02 美元。

I just read about Branch-Prediction and wanted to try how this works with Java 8 Streams

分支预测是一个 CPU 特性,它与 JVM 无关。需要保持 CPU 管道充满并准备好做某事。测量或 预测 分支预测非常困难(除非你真的知道 CPU 会做的事情)。这将至少取决于 CPU 现在的负载(可能比您的程序多得多)。

However the performance with Streams is always turning out to be worse than traditional loops

此声明与上一声明无关。是的,对于像您这样的简单 示例,流会变慢 ,最多慢 30%,这没关系。 您可以像其他人建议的那样,通过 JMH 测量 对于特定情况 它们有多慢或多快,但这仅证明了这种情况,仅证明了负载。

与此同时,您 可能正在 与 Spring/Hibernate/Services 等工作,这些工作以毫秒为单位,您的流以纳秒为单位,您担心性能?您在质疑代码中最快部分的速度?这当然是理论上的事情。

最后一点,您尝试使用已排序和未排序的数组,结果很糟糕。这绝对不是分支预测或不预测的迹象——你不知道预测发生在哪一点,如果它发生了 除非 你可以查看实际的 CPU 管道 - 这你没有。

我的 Java 程序如何 运行 快?

长话短说,Java 程序可以通过以下方式加速:

  1. 多线程
  2. JIT

流与 Java 程序加速有关吗?

是的!

  1. 注意 Collection.parallelStream() and Stream.parallel() 多线程方法
  2. 可以编写 for 足够长的周期让 JIT 跳过。 Lambda 通常很小,可以通过 JIT 编译 => 有可能提高性能

什么场景流可以比for循环更快?

我们来看看jdk/src/share/vm/runtime/globals.hpp

develop(intx, HugeMethodLimit,  8000,
        "Don't compile methods larger than this if "
        "+DontCompileHugeMethods")

如果你的周期足够长,它不会被JIT编译,会运行慢。 如果您将这样的循环重写为流式传输,您可能会使用 mapfilterflatMap 方法将代码拆分为多个部分,并且每个部分都可以足够小以适应限制。 当然,除了 JIT 编译之外,编写庞大的方法还有其他缺点。 例如,如果您有大量生成的代码,则可以考虑这种情况。

什么是分支预测?

当然,与其他所有代码一样,流也会利用分支预测。 然而,分支预测并不是明确用于使流更快 AFAIK 的技术。

那么,我什么时候将循环重写为流以获得最佳性能?

从来没有。

Premature optimization is the root of all evil ©Donald Knuth

尝试优化算法。流是函数式编程的接口,而不是加速循环的工具。