Search code examples
javaoption-typejit

JIT compilation of Optional


Not so much a question but an observation.

I'm somewhat surprised that the JIT compiler doesn't inline the use of the Optional class since it seems to be a heavily used part of the language since Java 8. I was expecting the following two test methods to perform equivalent:

    import static java.util.Optional.*;

    public static class TestClass {

        int i;

        public void test1(Integer x) {
            i = ofNullable(x).orElse(-1);
        }

        public void test2(Integer x) {
            if (x == null)
                i = -1;
            else
                i = x;
        }
    }

Instead, test1 always allocates an Optional for non-null values and is therefore 20x slower than test2. It just seems like this code should be easily optimized by the JIT compiler.

I tested this on Java 8 and Java 11. Anyone know if newer versions of Java do a better job at optimizing this? In general I like the terseness of using ofNullable over if/else statements, but I can't use them in critical code paths due to heavy GC.

Edit: Here's the benchmarking code I used:

@BenchmarkOptions(benchmarkRounds = 100, warmupRounds = 20)
public class BenchmarkTest {
    @Rule
    public TestRule benchmarkRun = new BenchmarkRule();

    public static Integer[] ARRAY;

    /** Prepare random numbers for tests. */
    @BeforeClass
    public static void beforeClass() {
        ARRAY = new Integer[10000000];
        for (int i = 0; i < 10000000; i++)
            ARRAY[i] = i % 2 == 0 ? null : i;
    }

    @Test
    public void test1() throws Exception {
        TestClass x = new TestClass();
        for (int a = 0; a < ARRAY.length; a++) {
            x.test1(ARRAY[a]);
        }
    }

    @Test
    public void test2() throws Exception {
        TestClass x = new TestClass();
        for (int a = 0; a < ARRAY.length; a++) {
            x.test2(ARRAY[a]);
        }
    }

Solution

  • If you want to use the more readable way like the Optional provides, but also want to control JIT. You coould write a simple class that JIT can easily inline.

    package util;
    
    
    public class OptionalInteger {
    
        public interface NullableInt {
            public int orElse(int defaultValue);
        }
    
        public static NullableInt ofNullable(Integer integer) {
            return new NullableInt() {
    
                @Override
                public int orElse(int elseValue) {
                    return integer == null ? elseValue : integer;
                }
            };
        }
    }
    

    Your client code will almost look the same.

    import static util.OptionalInteger.ofNullable;
    
    public class TestClass {
    
        int i;
    
        public void test1(Integer x) {
            i = ofNullable(x).orElse(-1);
        }
    
    }
    

    But JIT easily recognizes that it can be inlined.

    JIT elliminated allocations


    Here is the test code

    class Main {
        public static void main(String args[]) {
            List<Integer> integers = new ArrayList<>();
    
            Random random = new Random();
    
            for (int i = 0; i < 100000000; i++) {
                Integer inte = random.nextBoolean() ? random.nextInt() : null;
                integers.add(inte);
            }
    
            TestClass testClass = new TestClass();
    
            warmUpJIT(integers, testClass);
    
            execWithMeasurement(testClass, integers);
        }
    
        private static void warmUpJIT(List<Integer> integers, TestClass testClass) {
            exec(testClass, integers);
            exec(testClass, integers);
            exec(testClass, integers);
            exec(testClass, integers);
        }
    
        private static void execWithMeasurement(TestClass testClass, List<Integer> integers) {
            long start = System.currentTimeMillis();
    
            int result = exec(testClass, integers);
    
            long end = System.currentTimeMillis();
    
            String msg = MessageFormat.format("Result {0} - took {1} ms", result, (end - start));
            System.out.println(msg);
        }
    
        private static int exec(TestClass testClass, List<Integer> integers) {
            int result = 0;
    
            for (Integer integer : integers) {
                testClass.test1(integer);
                result += testClass.i;
            }
    
            return result;
        }
    }
    

    I used JITclipse, an eclipse integration for JITWatch that I wrote some time ago.