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

我刚刚阅读了有关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) 
    

我提到了几个博客这个这个,它建议相同的性能问题w.r.t流。

  1. 我同意使用流进行编程对于某些方案来说很好,更容易,但是当我们失去性能时,为什么我们需要使用它们呢?我错过了什么吗?
  2. 流执行等于循环的方案是什么?它是否仅在定义函数需要花费大量时间的情况下,导致循环性能可以忽略不计?
  3. 在任何一个场景中,我都看不到流利用分支预测(我尝试使用排序和无序流,但没有用。与普通流相比,它的性能影响是正常流多一倍)?

答案 1

我同意使用流进行编程对于某些方案来说很好,更容易,但是当我们失去性能时,为什么我们需要使用它们呢?

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

我错过了什么吗?

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

流执行等于循环的方案是什么?它是否仅在定义函数需要花费大量时间的情况下,导致循环性能可以忽略不计?

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

在任何一个场景中,我都看不到流利用分支预测

分支预测是 CPU 功能部件,而不是 JVM 或流功能部件。


答案 2

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

切勿出于性能原因选择某种方法,除非您已经证明这是实际应用程序中的问题。

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

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


推荐