Question
Java Arithmetic Performance: Why Parentheses Can Benchmark Differently
Question
Consider this Java program, which repeatedly evaluates 2 * (i * i):
public static void main(String[] args) {
long startTime = System.nanoTime();
int n = 0;
for (int i = 0; i < 1_000_000_000; i++) {
n += 2 * (i * i);
}
System.out.println(
(double) (System.nanoTime() - startTime) / 1_000_000_000 + " s"
);
System.out.println("n = " + n);
}
On one machine, this version takes about 0.50–0.55 seconds. Replacing the expression with the mathematically equivalent expression below takes about 0.60–0.65 seconds:
n += 2 * i * i;
Why can 2 * (i * i) appear faster than 2 * i * i in Java, even though both expressions produce the same int result?
Short Answer
You will learn how Java parses multiplication expressions, why these two expressions have equivalent int results, and why equivalent source code can still be compiled into different machine code. You will also learn why timing a single loop with System.nanoTime() is not a reliable way to compare tiny performance differences, and how to use JMH for Java microbenchmarks.
Concept
Java evaluates multiplication from left to right when parentheses do not override that order:
2 * i * i
// is parsed as:
(2 * i) * i
By contrast:
2 * (i * i)
explicitly computes i * i first.
For Java int values, both expressions have the same final result. int arithmetic wraps around on overflow using the low 32 bits of the result. Multiplication remains associative under this wraparound behavior, so these are equivalent:
2 * (i * i)
(2 * i) * i
However, Java source code is not executed directly by the processor. The JVM initially interprets code and then its Just-In-Time (JIT) compiler may compile hot code into native machine instructions. The JIT is allowed to optimize each expression shape differently as long as the observable Java behavior stays the same.
A particular JVM version, CPU architecture, compiler tier, and runtime state may produce different instruction sequences for these forms. For example, one form may be compiled using a multiply followed by an addition or shift, while another may have a different dependency chain. Small instruction-selection differences can matter in a loop that runs one billion times.
There is no Java-language rule saying that parentheses make arithmetic faster. On another JVM, Java version, processor, or run, the difference may shrink, disappear, or reverse.
Mental Model
Think of Java source code as a recipe and JIT compilation as a chef adapting that recipe to a particular kitchen.
Both recipes make the same dish:
- Recipe A: square
i, then double it. - Recipe B: double
i, then multiply byi.
The result is the same, but the chef may choose different tools, arrange steps differently, or reuse an intermediate result. In a kitchen preparing one meal, that difference is unimportant. In a loop preparing one billion meals, a tiny difference in the chosen steps can become measurable.
The important point is that the parentheses are not inherently a speed switch. They merely give the compiler a differently shaped expression to optimize.
Syntax and Examples
Multiplication has the same precedence on both sides, so Java evaluates chained * operators from left to right.
int i = 10;
int leftToRight = 2 * i * i; // Parsed as (2 * i) * i
int grouped = 2 * (i * i); // Explicitly compute i * i first
System.out.println(leftToRight); // 200
System.out.println(grouped); // 200
For ordinary mathematical values, both calculate 2 × i².
Showing the parse explicitly
int i = 7;
int a = (2 * i) * i;
int b = 2 * (i * i);
System.out.println(a == b); // true
With int, the equality also holds when intermediate values overflow because Java arithmetic wraps consistently at 32 bits.
Step by Step Execution
Consider this small example:
int i = 3;
int first = 2 * (i * i);
int second = 2 * i * i;
2 * (i * i)
- Evaluate the parentheses:
i * ibecomes3 * 3, which is9. - Multiply by
2:2 * 9becomes18. - Assign
18tofirst.
2 * i * i
- Java associates the operators from the left:
(2 * i) * i. - Compute
2 * 3, which is6. - Compute
6 * 3, which is .
Real World Use Cases
This topic appears whenever a program has a small calculation in code that runs very frequently.
- Graphics and games: Squared distances, coordinate transformations, and pixel calculations may run millions of times per frame.
- Data processing: Numeric transformations can run once for every row, event, or sensor reading.
- Financial or scientific simulations: Repeated formulas can make arithmetic and memory-access costs important.
- Serialization and compression: Bit operations and arithmetic occur in tight processing loops.
- Server applications: A seemingly small calculation may become significant when it runs for every request at high traffic levels.
In most applications, database calls, network requests, allocations, I/O, and poor algorithms matter far more than the placement of parentheses. Measure first and optimize the actual bottleneck.
Real Codebase Usage
In production Java code, developers usually focus on clear expressions and use performance tools only after profiling identifies a hot path.
Keep the intent readable
int areaScale = 2 * (radius * radius);
The grouping documents that the square is conceptually calculated before scaling.
Avoid repeated work when it is genuinely repeated
If the square is needed multiple times, store it:
int squared = i * i;
int doubledSquare = 2 * squared;
int tripledSquare = 3 * squared;
A modern JIT may do this optimization itself, but naming the intermediate value can improve readability.
Use wider types when overflow is not intended
long value = 2L * i * i;
The 2L makes the calculation use long arithmetic. This is about correctness, not necessarily speed.
Benchmark hot code with JMH
Common Mistakes
Assuming equivalent source must have identical timing
Equivalent Java expressions must produce the same observable result, but the JVM does not promise identical native code or execution time.
// Equivalent result does not imply identical machine instructions.
2 * (i * i);
2 * i * i;
Treating one main method timing as a microbenchmark
This approach includes startup and compilation behavior:
long start = System.nanoTime();
// Code that may be interpreted, then compiled while it runs
long elapsed = System.nanoTime() - start;
The loop may begin interpreted, be compiled at one optimization tier, then be recompiled at another tier. Garbage collection, operating-system scheduling, CPU frequency changes, and background processes can also affect the result.
Use JMH when comparing very small operations.
Believing alternating runs removes all bias
Alternating two executable runs is better than always running one version first, but it does not make samples independent or identically distributed. CPU temperature, turbo behavior, JIT state, and other system activity can still be correlated with run order.
Therefore, a simple probability calculation based on each result being an independent 50/50 event is not valid for this benchmark.
Forgetting integer overflow
Comparisons
| Topic | 2 * (i * i) | 2 * i * i |
|---|---|---|
| Source-level grouping | Square first, then multiply by 2 | Left-associative: (2 * i) * i |
Final int result | Same | Same |
| Overflow behavior | Wraps as int | Same final wrapped result |
| Readability | Emphasizes “twice the square” | Emphasizes a multiplication chain |
| Guaranteed performance difference | No | No |
| Possible JIT-generated code | May differ by JVM/CPU/run | May differ by JVM/CPU/run |
versus
Cheat Sheet
2 * i * iis parsed as(2 * i) * ibecause*associates left to right.2 * (i * i)explicitly evaluatesi * ifirst.- For Java
int, both expressions produce the same final value, including overflow behavior. - Parentheses can improve readability; they do not guarantee faster code.
- The JIT compiler can generate different machine code for equivalent source expressions.
- Actual performance depends on the JVM version, JIT tier, processor, operating system, and runtime conditions.
System.nanoTime()measures elapsed time, but it does not turn a one-off loop into a reliable microbenchmark.- Use JMH for microbenchmarks: warm up first, run multiple forks, and consume computed results.
- Use
2L * i * iwhen the calculation should uselongarithmetic. - Profile realistic applications before optimizing arithmetic expressions.
FAQ
Does Java always evaluate 2 * i * i from left to right?
At the language level, yes: it is parsed as (2 * i) * i. The JIT may later optimize the compiled implementation while preserving the result.
Are 2 * (i * i) and 2 * i * i always equal in Java?
For int arithmetic, yes. Both have the same 32-bit wrapped result. The same applies to long arithmetic with 64-bit wrapping.
Do parentheses make Java code faster?
No. Parentheses change grouping and can improve clarity, but Java does not guarantee that they improve performance. Any observed difference is JVM- and machine-dependent.
Why is System.nanoTime() not enough for a Java benchmark?
It gives a suitable elapsed-time clock, but it does not control JIT warmup, recompilation, garbage collection, CPU frequency changes, process scheduling, or statistical measurement.
What is JMH in Java?
JMH is the Java Microbenchmark Harness. It is built for measuring small JVM operations and manages warmup, measurement iterations, forks, and result consumption.
Can the JIT remove a loop from a benchmark?
Potentially, if it can prove the loop result is never observed. Keep results observable or use JMH's Blackhole.
Should I use long to make the expression faster?
Mini Project
Description
Create a JMH benchmark that compares the two equivalent arithmetic expressions correctly. The project demonstrates that a microbenchmark needs warmup, repeated measurement, and a consumed result before any performance conclusion is trustworthy.
Goal
Benchmark 2 * (i * i) and 2 * i * i using JMH and compare their measured throughput on your own JVM and machine.
Requirements
Use JMH annotations to define separate benchmark methods for the two expressions.
Run each expression inside a loop and consume the final result with Blackhole.
Use a configurable loop limit rather than hard-coding a value in the benchmark method.
Include warmup and measurement iterations.
Verify that both methods produce the same accumulated result before interpreting timing differences.
Keep learning
Related questions
Add External JAR Files to an IntelliJ IDEA Java Project
Learn how to add external JAR dependencies to an IntelliJ IDEA Java project using module libraries, and when to use Maven or Gradle instead.
Avoiding Java Code in JSP with JSP 2: EL and JSTL Explained
Learn how to avoid Java scriptlets in JSP 2 using Expression Language and JSTL, with examples, best practices, and common mistakes.
Call a Method After a Delay in Android Java
Learn how to run Java code after a delay in Android using Handler.postDelayed, manage the main thread, and cancel callbacks safely.