1. 概述

代码优化时,即使表达式语法的微小差异也可能影响性能。Java中2 * (i * i)2 * i * i就是典型例子。乍看两者似乎相同,但计算顺序的细微差别会导致性能差异。

本文将深入探讨为何2 * (i * i)通常更快,并揭示背后的技术原理。让我们开始吧。

2. 表达式解析

先拆解两个表达式:

带括号版本:

2 * (i * i)

计算顺序:先执行i * i,再将结果乘以2。

无括号版本:

2 * i * i

计算顺序:从左到右,先计算2 * i,再将结果乘以i

3. 性能对比

尽管两者理论结果相同,但运算顺序会影响实际性能:

3.1 编译器优化

JVM的JIT编译器在运行时优化代码,但优化效果依赖代码清晰度:

2 * (i * i)
括号明确指定运算顺序,编译器能更高效优化乘法操作。

2 * i * i
运算顺序不明确,可能导致编译器生成效率较低的优化方案。

关键点:括号为编译器提供了明确的计算路径指示,往往能生成更优的字节码。

3.2 整数溢出处理

当计算结果超过int最大值(2³¹-1)时会发生整数溢出。两种写法的溢出风险不同:

⚠️ 2 * i * i
i较大,中间结果2 * i可能接近溢出阈值,后续再乘i时风险更高。

2 * (i * i)
先计算平方值i * i,再判断是否需要乘2,对大数值场景更安全。

3.3 CPU执行效率

CPU指令执行顺序受运算分组影响:

2 * (i * i)
CPU可高效处理平方运算(i * i),指令流水更顺畅。

2 * i * i
中间结果2 * i可能占用额外寄存器,尤其当值较大时增加CPU周期。

实际影响

  • 小数值场景差异可忽略
  • 大数值或高频循环场景差异显著

4. JMH性能测试

用JMH(Java微基准测试工具)验证理论。测试参数配置:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class MultiplicationBenchmark {
    private int smallValue = 255;
    private int largeValue = 2187657;

    @Benchmark
    public int testSmallValueWithParentheses() {
        return 2 * (smallValue * smallValue);
    }

    @Benchmark
    public int testSmallValueWithoutParentheses() {
        return 2 * smallValue * smallValue;
    }

    @Benchmark
    public int testLargeValueWithParentheses() {
        return 2 * (largeValue * largeValue);
    }

    @Benchmark
    public int testLargeValueWithoutParentheses() {
        return 2 * largeValue * largeValue;
    }
}

测试结果(单位:纳秒/操作):

Benchmark                                             Mode  Cnt  Score   Error  Units
MultiplicationBenchmark.largeValueWithParentheses     avgt    5  1.066 ± 0.168  ns/op
MultiplicationBenchmark.largeValueWithoutParentheses  avgt    5  1.283 ± 0.392  ns/op
MultiplicationBenchmark.smallValueWithParentheses     avgt    5  1.173 ± 0.218  ns/op
MultiplicationBenchmark.smallValueWithoutParentheses  avgt    5  1.222 ± 0.287  ns/op

结果分析: | 测试场景 | 平均耗时 | 性能对比 | |---------|---------|---------| | 大数值+括号 | 1.066 ns/op | ⚡ 最快 | | 大数值无括号 | 1.283 ns/op | 🐌 最慢 | | 小数值+括号 | 1.173 ns/op | ✅ 略优 | | 小数值无括号 | 1.222 ns/op | ❌ 略差 |

关键发现
括号版本在大数值场景快20%,小数值场景也有微弱优势

5. 结论

虽然2 * (i * i)2 * i * i结果相同,但前者通常更快。括号带来的性能优势体现在:

  1. ✅ 更优的编译器优化空间
  2. ✅ 更可预测的中间结果
  3. ✅ 降低整数溢出风险
  4. ✅ 更高效的CPU指令执行

开发建议:在性能敏感代码中,不仅要关注正确性,还需考虑执行方式。像括号位置这样的微小改动,可能带来显著性能提升——这正体现了理解语言底层机制的重要性。


原始标题:Why Is 2 * (i * i) Faster Than 2 * i * i in Java? | Baeldung