上周,我介绍了一些有关Java 8流性能的基准测试结果。 你们和gal足够感兴趣,可以留下一些想法,还有哪些可以介绍。
这就是我所做的,这是结果。
总览
最后一篇文章的序言也适用于此。 阅读它,以找出所有数字为何撒谎,我如何提出这些数字以及如何复制它们。
我在GitHub上的代码中添加了一个新类CommentOperationsBenchmark ,其中包括本文中讨论的基准。 我还更新了Google电子表格,以包括新的数字。
比较的影响
真好 长期以来一直在说将Java写成Ansi C更快(数组而不是列表)。
兔子洞的下一步是…
尝试{for(int i = 0 ;;)做东西; } catch(ex ex ex){等等! }
根本不检查循环,仅捕获异常,非常适合高清像素处理。
混沌实验室
WAT? 人们正在这样做吗?
打破ArrayIndexOotOfBoundsException
public int array_max_forWithException() {int m = Integer.MIN_VALUE;try {for (int i = 0; ; i++)if (intArray[i] > m)m = intArray[i];} catch (ArrayIndexOutOfBoundsException ex) {return m;}
}
也许他们应该停下来,因为它看起来并不能改善性能:
以毫秒为单位的运行时间标准化为1'000'000个元素 | ||||||
---|---|---|---|---|---|---|
5万 | 500'000 | 1'000'000 | 5'000'000 | 10'000'000 | 50'000'000 | |
array_max_for | 0.261 | 0.261 | 0.277 | 0.362 | 0.347 | 0.380 |
array_max_forWithException | 0.265 | 0.265 | 0.273 | 0.358 | 0.347 | 0.386 |
看来,用于打破循环的机制没有可衡量的影响。 这是有道理的,因为循环展开可以避免大多数比较,并且引发异常的代价在几微秒的范围内 ,因此比此处发生的情况小几个数量级。
这是假设编译器确实有更多技巧。 也许它更深刻地理解了循环,并且JIT将这两种方法编译为相同的指令。
附带说明一下:看看循环后array_max_forWithException如何没有return语句?
事实证明Java编译器可以识别简单的无限循环 。 哇! 因此,它知道具有有限计算的每个代码路径都会返回,而不关心无限的代码路径。
归结为以下内容:
一无所有
public int infiniteLoop() {for(;;);
}
您永不停止学习……
作业的影响
[F]或“最大”测试我希望每次迭代更新局部变量都会有一些阻力。 我很好奇寻找最小值是否在可比较的时间内运行。b0b0b0b
这是指所有测试都是在数组或列表上运行的,这些数组或列表的元素等于结构中的索引,即[0,1,2,…,n-1]。 因此,找到最大值确实需要n个分配。
那找到一个只需要一个作业的最小值呢?
以毫秒为单位的运行时间标准化为1'000'000个元素 | ||||||
---|---|---|---|---|---|---|
5万 | 500'000 | 1'000'000 | 5'000'000 | 10'000'000 | 50'000'000 | |
array_max_for | 0.261 | 0.261 | 0.277 | 0.362 | 0.347 | 0.380 |
array_min_for | 0.264 | 0.260 | 0.280 | 0.353 | 0.348 | 0.359 |
不,没有区别。 我的猜测是,由于流水线作业,分配实际上是免费的。
拳击的影响
关于拳击有两条评论。
最好看到Integer []实现,以确认对拳击的怀疑。
黏黏的
好吧,让我们这样做。 以下数字显示了int [],Integer []和List <Integer>上的for循环和for-each循环:
以毫秒为单位的运行时间标准化为1'000'000个元素 | ||||||
---|---|---|---|---|---|---|
5万 | 500'000 | 1'000'000 | 5'000'000 | 10'000'000 | 50'000'000 | |
array_max_for | 0.261 | 0.261 | 0.277 | 0.362 | 0.347 | 0.380 |
array_max_forEach | 0.269 | 0.262 | 0.271 | 0.349 | 0.349 | 0.356 |
boxedArray_max_for | 0.804 | 1.180 | 1.355 | 1.387 | 1.306 | 1.476 |
boxedArray_max_forEach | 0.805 | 1.195 | 1.338 | 1.405 | 1.292 | 1.421 |
list_max_for | 0.921 | 1.306 | 1.436 | 1.644 | 1.509 | 1.604 |
list_max_forEach | 1.042 | 1.472 | 1.579 | 1.704 | 1.561 | 1.629 |
我们可以清楚地看到,运行时的主要指标是数据结构是包含基元还是对象。 但是将Integer数组包装到列表中会导致额外的速度降低。
Yann Le Tallec也对拳击发表了评论:
intList.stream()。max(Math :: max); 造成不必要的拆箱。
intList.stream()。mapToInt(x-> x).max(); 速度大约是阵列版本的两倍。扬·勒·塔雷克(Yann Le Tallec)
此声明与我们在上一篇文章中得出的结论一致:尽快对流取消装箱可能会提高性能。
只是再次检查:
以毫秒为单位的运行时间标准化为1'000'000个元素(以%为单位的错误) | ||||||
---|---|---|---|---|---|---|
5万 | 500'000 | 1'000'000 | 5'000'000 | 10'000'000 | 50'000'000 | |
boxedArray_max _stream | 4.231(43%) | 5.715(3%) | 5.004(27%) | 5.461(53%) | 5.307(56%) | 5.507(54%) |
boxedArray_max _stream_unbox | 3.367(<1%) | 3.515(<1%) | 3.548(2%) | 3.632(1%) | 3.547(1%) | 3.600(2%) |
list_max _stream | 7.230(7%) | 6.492(<1%) | 5.595(36%) | 5.619(48%) | 5.852(45%) | 5.631(51%) |
list_max _stream_unbox | 3.370(<1%) | 3.515(1%) | 3.527(<1%) | 3.668(3%) | 3.807(2%) | 3.702(5%) |
这似乎证实了这一说法。 但是结果看起来非常可疑,因为错误很大。 使用不同的设置反复运行这些基准测试显示了一种模式:
- 存在两种性能水平,一种是〜3.8 ns / op,一种是〜7.5 ns / op。
- 未装箱的流只表现更好。
- 盒装流的单个迭代通常在这两个级别中的任何一个上运行,但很少在另一个时间运行。
- 大多数情况下,行为只会在分支之间变化(即从一组迭代到下一组)。
这一切都令人怀疑我的测试设置存在问题。 听到任何想法的人,我会很有趣。
更新资料
Yann确实有一个主意,并指出了这个有趣的问题和有关StackOverflow的出色答案 。 现在,我最好的猜测是,装箱的流可以在未装箱的水平上运行,但可能会因意外的非优化而祈祷。
硬件的影响
Redditor robi2106在其“ i5-4310 @ 2Ghz w 8GB DDR2”上运行了500'000个元素的套件。 我将结果添加到电子表格中 。
很难从数据中得出结论。 Robi指出“在这2.5个小时中我也没有停止使用我的系统”,这也许可以解释巨大的误差范围。 他们的平均年龄是我的23倍,平均是我的168倍。 (另一方面,我也继续使用我的系统,但是负载很低。)
如果斜视一下,可以推断出i5-4310在简单的计算上会稍快一些,但在更复杂的计算上会落后一些。 考虑到i7-4800的内核数量是原来的两倍,通常可以达到并行性能。
语言的影响
这与Scala(带有@specialized)相比,会很有趣。
加密货币6
我仍然没有尝试使用Scala,也不想为一个基准测试而努力。 也许经验更丰富或更鲜美的人可以尝试一下?
反射
在解释这些数字时,请记住,这些迭代执行了非常便宜的操作。 上次我们发现已经很简单的算术运算会导致足够的CPU负载, 几乎完全抵消了迭代机制的差异 。 因此,像往常一样,不要过早优化!
总而言之,我会说:没有新发现。 但是,我很喜欢与您讨论您的想法,如果您有更多想法,请发表评论。 甚至更好的是,自己尝试一下并发布结果。
翻译自: https://www.javacodegeeks.com/2015/09/stream-performance-your-ideas.html