什么时候应该比传统循环更优先selectstream? stream是利用分支预测吗?
我刚刚阅读了关于分支预测的内容 ,想试试Java 8 Streams如何工作。
然而Streams的performance总是比传统的循环更糟糕。
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));
输出:
-
对于sortingarrays:
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)
-
对于非sortingarrays:
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.stream()
而不是Arrays.stream(array)
list.get(c)
而不是array[c]
输出:
-
对于Sorted-List:
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)
-
对于未分类列表
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)
我提到了一些博客, 这表明了同样的性能问题。
- 我同意这样一个观点,即在某些情况下,使用stream编程对于编程来说很好,但是当我们失去了性能时,为什么我们需要使用它们呢? 有什么我错过了吗?
- 哪种情况下,stream执行等于循环? 只有在你的函数定义需要很多时间的情况下,导致循环性能可以忽略不计?
- 在这个场景中,我没有看到利用分支预测的stream(我尝试过使用有序stream和无序stream,但是没用,与普通stream相比,性能影响是两倍多)?
我同意这样一个观点,即在某些情况下,使用stream编程对于编程来说很好,但是当我们失去了性能时,为什么我们需要使用它们呢?
性能很less是一个问题。 通常情况下,10%的数据stream需要重写为循环才能获得所需的性能。
有什么我错过了吗?
使用parallelStream()更容易使用stream,可能更有效,因为很难编写高效的并发代码。
哪种情况下,stream执行等于循环? 只有在你的函数定义需要很多时间的情况下,导致循环性能可以忽略不计?
您的基准是有缺陷的,因为代码在启动时尚未编译。 我会像JMH一样在循环中完成整个testing,否则我会使用JMH。
在这种情况下,我都没有看到stream利用分支预测
分支预测是CPUfunction而不是JVM或streamfunction。
Java是一种高级语言,可以帮助程序员从低层次的性能优化中解脱出来。
除非您已经certificate这是实际应用中的问题,否则不要select某种性能方面的方法。
您的测量结果显示了对stream的一些负面影响,但差异在可观察性以下。 因此,这不是一个问题。 而且,这个testing是一个“综合”的情况,代码在重型生产环境中可能performance完全不同。 此外,由Java(字节)代码通过JIT创build的机器代码可能会在将来的Java(维护)版本中发生变化,并使您的测量过时。
总结:select最能expression你的 (程序员) 意图的语法或方法。 除非您有充分的理由改变,否则在整个计划中保持相同的方法或语法。
一切都说了,但我想告诉你如何使用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; } }
sorting数组的结果:
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优化有很多可能性,也许涉及到分支预测。 现在由您来解释基准testing结果。
我将在这里添加我的0.02 $。
我刚刚阅读了关于分支预测的内容,想试试Java 8 Streams如何工作
分支预测是一个CPU特性,与JVM无关。 需要保持CPUpipe道充足,并准备好做些事情。 测量或者预测分支预测是非常困难的(除非你真的知道CPU将要做的事情)。 这至less取决于CPU现在所拥有的负载(可能比你的程序要多得多)。
然而,Streams的performance总是比传统的循环更糟糕
这个声明和以前的声明是不相关的。 是的, 对于像你们这样的简单例子,数据stream会比较慢,最多可以减慢30%,这是可以的。 您可以像其他人所说的那样通过JMH测量某个特定情况下的速度是多快还是更快,但是仅certificate了这种情况,只有这个负载。
与此同时,你可能正在使用Spring / Hibernate / Services等等,以毫秒为单位来做事情,在纳秒级做你的stream,你担心性能? 你在质疑代码中最快的部分的速度? 这当然是理论上的事情。
关于你最后一点,你尝试与sorting和未sorting的数组,它会给你不好的结果。 这绝对不是分支预测的指标 – 你不知道在哪一点预测发生,如果没有,你可以看看真正的CPUpipe道内部 – 你没有。
我的Java程序如何运行?
长话短说,Java程序可以通过以下方式加速:
- multithreading
- JIT
stream是否与Java程序加速有关?
是!
- 注意multithreading的
Collection.parallelStream()
和Stream.parallel()
方法 - 人们可以写出足够长的周期以供JIT跳过。 兰姆达斯通常很小,可以通过JIT =>编译,有可能获得性能
什么是情景stream可以比循环更快?
我们来看看jdk / src / share / vm / runtime / globals.hpp
develop(intx, HugeMethodLimit, 8000, "Don't compile methods larger than this if " "+DontCompileHugeMethods")
如果你有足够长的周期,它将不会被JIT编译并且运行缓慢。 如果你重写这样一个循环来stream,你可能会使用map
, filter
, flatMap
方法将代码拆分成小块,每一块都可以小到足够小。 当然,编写巨大的方法除了JIT编译之外还有其他的缺点。 例如,如果您有大量生成的代码,则可以考虑这种情况。
什么是分支预测?
当然,与其他代码一样,stream利用分支预测。 然而,分支预测并不是明确用于使stream更快地AFAIK的技术。
那么,我什么时候可以重写我的循环以获得最佳性能?
决不。
过早的优化是所有邪恶的根源© Donald Knuth
尝试优化algorithm。 stream是function类编程的接口,而不是加速循环的工具。