Constant Propagation in the JVM
What is Constant Propagation?
Constant propagation is a compiler optimization technique where the compiler identifies variables that have constant values and replaces their uses with the actual constants. This optimization can happen both at compile-time (javac) and runtime (JIT).
Basic Example
Before Optimization
void example() {
final int x = 10;
int y = x + 5;
int z = y * 2;
}
After Optimization
void example() {
final int x = 10; // Known constant
int y = 15; // Propagated: 10 + 5
int z = 30; // Propagated: 15 * 2
}
Types of Constant Propagation
1. Simple constant propagation
public class SimpleExample {
private static final int MULTIPLIER = 5;
public int calculate(int input) {
int factor = MULTIPLIER; // Will be propagated
return input * factor; // Becomes: return input * 5;
}
}
2. Copy propagation
public class CopyExample {
public int calculate() {
int a = 42;
int b = a; // Copy
int c = b; // Another copy
return c; // Becomes: return 42;
}
}
3. Conditional constant propagation
public class ConditionalExample {
public int getValue(boolean condition) {
int x;
if (condition) {
x = 10;
} else {
x = 10; // Same value in both branches
}
return x; // Can be optimized to: return 10;
}
}
JIT Compiler Optimizations
The JIT compiler can perform more aggressive constant propagation because it has runtime information:
public class JITExample {
private int value = 100;
public int calculate() {
// JIT can see that value never changes
return value * 2; // Might optimize to return 200
}
}
Complex examples
1. Method Inlining with Constant Propagation
- The JVM’s JIT compiler uses constant propagation to determine when method parameters are constants
- When parameters are known constants, it can better predict the behavior of the method
- This helps the JIT make more informed decisions about whether inlining will be beneficial
- For example, if constant propagation shows that a condition always evaluates to
true
, the JIT might inline the method and eliminate dead code branches
More about Method Inlining in JVM.
public class ComplexExample {
private static final int FACTOR = 10;
private int multiply(int x) {
return x * FACTOR;
}
public int process(int value) {
int result = multiply(value); // Will be inlined
return result + FACTOR; // Becomes: return value * 10 + 10;
}
}
2. Loop optimization
- Constant propagation helps identify loop bounds and step values that are constants
- When loop limits are known constants, the JIT can better determine if unrolling is worthwhile
- It can calculate the exact number of iterations and optimize accordingly
- This also helps in eliminating bounds checking in some cases
More about Loop unrolling in JVM.
public class LoopExample {
private static final int SIZE = 100;
public int[] createArray() {
int[] array = new int[SIZE];
for (int i = 0; i < SIZE; i++) { // SIZE is propagated
array[i] = i * SIZE; // SIZE is propagated
}
return array;
}
}
3. Dead code elimination
if (false) { // Constant condition
expensiveCall(); // Will be eliminated
}
4. Loop unrolling
final int SIZE = 4;
for (int i = 0; i < SIZE; i++) { // Known size
// Can be unrolled completely
}
5. Value range analysis
Value Range Analysis is an optimization technique where the JVM tracks what possible values a variable can have at each point in the program. It’s particularly important for eliminating array bounds checks.
final int ARRAY_SIZE = 100;
int[] array = new int[ARRAY_SIZE];
// Bounds check can be eliminated in many cases
for (int i = 0; i < ARRAY_SIZE; i++) {
array[i] = i;
}
// JVM can't eliminate bounds check
void example2(int x) {
int[] array = new int[100];
array[x] = 42; // Must check bounds!
// x could be anything
}
Best Practices
Use final when possible
// Good - helps compiler optimize
private static final int CONSTANT = 100;
// Less optimal - compiler might not optimize
private static int variable = 100;
Keep methods simple
// Good - easy to optimize
public int calculate(int x) {
final int factor = 10;
return x * factor;
}
// Less optimal - harder to optimize
public int calculate(int x) {
int factor = getFactor();
return x * factor;
}
Verification
You can verify constant propagation using JVM flags:
java -XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintInlining \
-XX:CompileCommand=print,*YourProgram.aSpecificMethod \
YourProgram
Performance Impact
Consider this example:
public class PerformanceExample {
private static final int LOOPS = 1_000_000;
private static final int MULTIPLIER = 7;
public long test() {
long sum = 0;
for (int i = 0; i < LOOPS; i++) {
// Constants will be propagated
sum += i * MULTIPLIER;
}
return sum;
}
public static void main(String[] args) {
new PerformanceExample().test();
}
}
Without constant propagation, each iteration would need to:
- Load
MULTIPLIER
value - Perform multiplication
- Add to sum
With constant propagation:
MULTIPLIER
is directly embedded in instructions- Better instruction cache utilization
- Potentially better loop optimization
When we compile, run the code, and see the optimization:
➜ java -XX:+PrintCompilation \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintInlining \
-XX:+PrintAssembly \
-XX:CompileCommand=print,*PerformanceExample.test \
PerformanceExample
OpenJDK 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
CompileCommand: print *PerformanceExample.test bool print = true
OpenJDK 64-Bit Server VM warning: CompileCommand and/or .hotspot_compiler file contains 'print' commands, but PrintAssembly is also enabled
...
You’ll need the hsdis
(HotSpot disassembler) plugin for PrintAssembly to work. See How to install HotSpot Disassembler (hsdis) on macOS for instructions how to install.
Here is a simplified output, where we use Inst (instruction) <number> instead of pointers:
# Initialize loop variables
0x000000010d8509e0: movz w0, #0, lsl #16 ; Initialize i = 0
0x000000010d8509e4: mov x1, #0 ; Initialize sum = 0
0x000000010d8509e8: b 0x10d850a44 ; Jump to loop condition
# Loop body - Here's where MULTIPLIER=7 constant propagation is visible
0x000000010d8509ec: lsl w2, w0, #3 ; w2 = i * 8 (multiply by shifting left 3)
0x000000010d8509f0: sub w2, w2, w0 ; w2 = (i * 8) - i = i * 7
; This is our MULTIPLIER=7 optimization!
0x000000010d8509f4: sxtw x2, w2 ; Sign extend to 64 bits
0x000000010d8509f8: add x1, x2, x1 ; sum += (i * 7)
0x000000010d8509fc: add w0, w0, #1 ; i++
# Loop condition with LOOPS=1_000_000 constant propagation
0x000000010d850a44: mov w2, #0x4240 ; Load lower 16 bits of 1_000_000
0x000000010d850a48: movk w2, #0xf, lsl #16 ; Load upper 16 bits to complete 1_000_000
0x000000010d850a4c: cmp w0, w2 ; Compare i with 1_000_000
Both constants are propagated into the instructions themselves rather than being loaded from memory, resulting in more efficient code execution.
MULTIPLIER=7
optimization:
Instead of loading 7 from memory, it uses shift-subtract sequence (because it is more efficient for CPUs):
lsl w2, w0, #3
multiplies by 8 (shift left by 3)sub w2, w2, w0
subtracts once to get multiply by 7- No memory load or immediate multiplication instruction needed
LOOPS=1_000_000
constant:
- Directly embedded in instructions as
#0x4240
and#0xf
- No memory loads needed to get the loop limit
- Uses a two-instruction sequence to build the 32-bit constant
0xf4240
= 1,000,000 in decimal