Question

Java Arithmetic Performance: Why Parentheses Can Benchmark Differently

javaperformancebenchmarkingbytecodejit

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 by i.

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.

Take Quiz

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.

Prefer clarity before micro-optimization

If the intent is “two times the square of i,” this is often clearest:

int twiceSquare = 2 * (i * i);

Choose the expression that communicates the intended calculation. Only choose a less clear form for performance after measuring a realistic workload with a reliable benchmark.

Step by Step Execution

Consider this small example:

int i = 3;

int first = 2 * (i * i);
int second = 2 * i * i;

2 * (i * i)

  1. Evaluate the parentheses: i * i becomes 3 * 3, which is 9.
  2. Multiply by 2: 2 * 9 becomes 18.
  3. Assign 18 to first.

2 * i * i

  1. Java associates the operators from the left: (2 * i) * i.
  2. Compute 2 * 3, which is 6.
  3. Compute 6 * 3, which is 18.
  4. Assign 18 to second.

Both variables contain 18.

In a hot loop, the JIT may not preserve these source-level steps literally. It can replace them with equivalent machine-level operations. That optimized code, rather than the apparent number of operators in the Java source, determines the runtime cost.

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.

Take Quiz

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

JMH handles warmup, repeated measurements, forks, and result reporting. It is the standard tool for measuring small Java code fragments because JVM execution changes as code becomes hot and gets compiled.

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

int value = 50_000 * 50_000; // Overflows; it is not 2,500,000,000 as an int

If the intended result must not overflow, use long:

long value = 50_000L * 50_000L;

Removing the result entirely

A benchmark must use the computed result. Otherwise, the JIT may prove that the calculation has no observable effect and remove some or all of it.

// Risky: the result is discarded.
for (int i = 0; i < limit; i++) {
    2 * (i * i);
}

JMH's Blackhole is designed to consume results safely.

Comparisons

Topic2 * (i * i)2 * i * i
Source-level groupingSquare first, then multiply by 2Left-associative: (2 * i) * i
Final int resultSameSame
Overflow behaviorWraps as intSame final wrapped result
ReadabilityEmphasizes “twice the square”Emphasizes a multiplication chain
Guaranteed performance differenceNoNo
Possible JIT-generated codeMay differ by JVM/CPU/runMay differ by JVM/CPU/run

int versus long

TypeRange behaviorExample
int32-bit; overflow wraps2 * i * i
long64-bit; much larger range2L * i * i

Use long when preserving larger numeric results is required. Do not switch types solely because one quick timing appeared faster; measure the complete workload.

Take Quiz

Cheat Sheet

  • 2 * i * i is parsed as (2 * i) * i because * associates left to right.
  • 2 * (i * i) explicitly evaluates i * i first.
  • 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 * i when the calculation should use long arithmetic.
  • 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?

Use long when correctness requires a larger range. Its performance characteristics depend on the workload and hardware; it is not a universal optimization.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more