这表明括号可能会使乘法略微优化,但性能差异很小。它提供了更可预测的中间结果、更好的编译器优化机会、更低的溢出风险和更高效的 CPU 执行。在编写性能至关重要的代码时,我们不仅应该考虑正确性,还应该考虑代码的执行方式。括号的位置等小变化可能会显著影响性能,这表明了解语言和硬件的机制非常重要。
在优化代码时,即使表达式语法中的微小差异也会影响性能。Java中的2 * (i * i)和2 * i * i之间的差异就是一个这样的例子。乍一看,这两个表达式可能看起来完全相同,但它们求值方式的细微差异可能会导致性能差异。
在本教程中,我们将探讨为什么2 * (i * i)通常比2 * i * i更快,并深入探究其根本原因。让我们开始吧。
理解表达
让我们分解一下这两个表达式。在这个表达式中,首先进行i * i的乘法,然后将结果乘以2:
2 * (i * i) |
在这个表达式中,计算从左到右进行:
2 * i * i |
首先计算2 * i,然后将结果乘以i。
性能比较
尽管从理论上讲,这两个表达式会产生相同的结果,但运算顺序会影响性能。
编译器优化
Java编译器(例如JVM中的即时 ( JIT ) 编译器)非常复杂,可以在运行时优化代码。但是,编译器在很大程度上依赖于代码的清晰度来进行优化:
- 2 * (i * i):括号明确定义了运算顺序,使编译器更容易优化乘法。
- 2 * i * i:运算顺序不够明确可能会导致优化效率降低。编译器优化代码的效率可能不如2 * (i * i)。
整数溢出注意事项
当计算产生的值大于 int可以存储的最大值(对于 32 位整数,为 2^31 – 1)时,就会发生整数溢出。虽然这两个表达式本身都不会导致溢出,但计算的结构方式会影响溢出的处理方式:
- 2 * i * i:如果i很大,2 * i的中间结果可能会接近溢出阈值。这可能会导致在最终乘以i时出现潜在问题。
- 2 * (i * i):在这个表达式中,我们更好地理解了乘以2是否会导致在对i 进行平方后溢出。因此,这使得表达式在涉及大值的场景中稍微安全一些。
CPU 级执行
在 CPU 级别,指令按特定顺序执行,该顺序根据操作的分组方式而变化:
- 2 * (i * i):CPU 可能会更好地优化此操作。因为平方(i * i)更直接。
- 2 * i * i :CPU 可能需要额外的周期来处理2 * i的中间结果,尤其是当这个结果很大时。
使用JMH进行性能测试
现在,为了证明这一理论,让我们用实际数据来做实验。 更准确地说,我们将展示最常见的集合操作的JMH (Java Microbenchmark Harness) 测试结果。
首先,我们将介绍基准测试的主要参数:
@State(Scope.Thread) |
然后我们将预热迭代次数设置为3。
现在,是时候添加以下小值和大值的基准测试了:
private int smallValue = 255; |
以下是带括号和不带括号的计算的测试结果:
Benchmark Mode Cnt Score Error Units |
结果显示了根据是否存在括号,不同乘法场景所需的平均时间(以每操作纳秒为单位, ns/op )。
具体如下:
MultiplicationBenchmark.largeValueWithParentheses(1.066±0.168 ns/op): |
这表示用括号将大值相乘。
- 平均耗时1.066纳秒,误差幅度为±0.168。
MultiplicationBenchmark.largeValueWithoutParentheses(1.283±0.392 ns/op): |
这表示不用括号将大值相乘。
- 平均时间为 1.283 纳秒,误差幅度为 ±0.392。
MultiplicationBenchmark.smallValueWithParentheses(1.173±0.218 ns/op): |
这表示用括号将小值相乘。
- 平均时间为 1.173 纳秒,误差幅度为 ±0.218。
MultiplicationBenchmark.smallValueWithoutParentheses(1.222±0.287 ns/op): |
这表示不用括号将小值相乘。
- 平均时间为 1.222 纳秒,误差幅度为 ±0.287。
更快的方法:带括号的大值的乘法是最快的(1.066 ns/op)。
更慢的方法:不带括号的大值花费的时间最多(1.283 ns/op)。