HopefullyHelpful HopefullyHelpful - 19 days ago 5
Java Question

Does the compiler/JIT recognize redundant checks?

Lets say we have multiple applications layers, external libraries and the java standard libraries and we are dealing with a string.

At each and every command when you hand in a nullpointer, the application is going to throw an exception. To prevent that, you are supposed to check for a null object and handle it yourself.

Now that means that you check for null, the external libraries check for null and the java standard libraries check for null and potentially some low level c code checks for null.

Isn't that very redundant ? Is this somehow optimized by the compiler and does the branch prediciton normally handle those cases well ?

Answer

As indicated by the comments and the other answers: There are many degrees of freedom and variables involved here. On the highest level, this refers to design principles, aiming at avoiding "meaningful" null values, and thus, the explicit null checks. (This is difficult sometimes. Some semantically meaningful cases of null are built into the standard API). It was also said that redundant null checks can not always be avoided, because one never knows from where a method will be called. On lower, technical levels, the distinction between the javac compiler and the JIT was pointed out. On the lowest level, things like branch prediction may come into play (this has become remarkably famous due to this array processing question...).


Referring to the JIT, I was curious about one case that may be particularly interesting for that pattern that you described - namely, whether redundant null checks are eliminated during method inlining.

I tried to create a simple test for this. But it's harder than expected to create a really sensible and meaningful test here: My idea was to create a very simple version of the chain of method calls that you suggested:

public static int processStringA(DummyString string)
{
    if (string == null) return -2;
    return processStringB(string);
}

public static int processStringB(DummyString string)
{
    if (string == null) return -3;
    return processStringC(string);
}

public static int processStringC(DummyString string)
{
    if (string == null) return -4;
    return string.value;
}

I spread this over several classes, and added some "dummy instructions" to make it less trivial (but still allow inlining), and ran the following test eventually:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;


public class NestedNullCheckTest
{
    public static void main(String[] args)
    {
        for (int i=0; i<1000; i++)
        {
            runTest();
        }
    }

    private static void runTest()
    {
        List<DummyString> list = createList();
        int blackHole = 0;
        for (DummyString string : list)
        {
            blackHole += processStringTest(string);
        }
        System.out.println("Result "+blackHole);
    }


    private static int processStringTest(DummyString string)
    {
        if (string == null)
        {
            return -1;
        }
        return NestedNullCheckA.processStringA(string);
    }


    private static List<DummyString> createList()
    {
        List<DummyString> list = new ArrayList<DummyString>();
        Random random = new Random(0);

        for (int i=0; i<100000; i++)
        {
            if (random.nextDouble() < 0.1)
            {
                list.add(null);
            }
            else
            {
                list.add(new DummyString(i));
            }
        }
        return list;
    }
}

class DummyString
{
    int value;
    DummyString(int value)
    {
        this.value = value;
    }
}

class NestedNullCheckA
{
    public static int processStringA(DummyString string)
    {
        if (string == null)
        {
            return -2;
        }
        string.value += 1;
        return NestedNullCheckB.processStringB(string);
    }
}


class NestedNullCheckB
{
    public static int processStringB(DummyString string)
    {
        if (string == null)
        {
            return -3;
        }
        string.value -= 2;
        return NestedNullCheckC.processStringC(string);
    }
}


class NestedNullCheckC
{
    public static int processStringC(DummyString string)
    {
        if (string == null)
        {
            return -4;
        }
        string.value *= 2;
        return string.value;
    }
}

Running this with

java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly NestedNullCheckTest 

eventually spilled out the following assembly for the processStringTest method:

Decoding compiled method 0x00b51488:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x3d90046c} &apos;processStringTest&apos; &apos;(LDummyString;)I&apos; in &apos;NestedNullCheckTest&apos;
  # parm0:    ecx       = &apos;DummyString&apos;
  #           [sp+0x10]  (sp of caller)
  0x00b51580: sub    $0xc,%esp
  0x00b51586: mov    %ebp,0x8(%esp)     ;*synchronization entry
                                        ; - NestedNullCheckTest::processStringTest@-1 (line 30)

  0x00b5158a: test   %ecx,%ecx
  0x00b5158c: je     0x00b515a4         ;*ifnonnull
                                        ; - NestedNullCheckTest::processStringTest@1 (line 30)

  0x00b5158e: mov    0x8(%ecx),%eax
  0x00b51591: shl    %eax
  0x00b51593: add    $0xfffffffe,%eax   ;*imul
                                        ; - NestedNullCheckC::processStringC@13 (line 103)
                                        ; - NestedNullCheckB::processStringB@18 (line 90)
                                        ; - NestedNullCheckA::processStringA@18 (line 76)
                                        ; - NestedNullCheckTest::processStringTest@7 (line 34)

  0x00b51596: mov    %eax,0x8(%ecx)     ;*putfield value
                                        ; - NestedNullCheckC::processStringC@14 (line 103)
                                        ; - NestedNullCheckB::processStringB@18 (line 90)
                                        ; - NestedNullCheckA::processStringA@18 (line 76)
                                        ; - NestedNullCheckTest::processStringTest@7 (line 34)

  0x00b51599: add    $0x8,%esp
  0x00b5159c: pop    %ebp
  0x00b5159d: test   %eax,0x970000      ;   {poll_return}
  0x00b515a3: ret    
  0x00b515a4: mov    $0xffffffff,%eax
  0x00b515a9: jmp    0x00b51599
  0x00b515ab: hlt    
  ...
  0x00b515bf: hlt    
[Exception Handler]
[Stub Code]
  0x00b515c0: jmp    0x00af5e40         ;   {no_reloc}
[Deopt Handler Code]
  0x00b515c5: push   $0xb515c5          ;   {section_word}
  0x00b515ca: jmp    0x00adbfc0         ;   {runtime_call}
  0x00b515cf: hlt    

Take it with a huge grain of salt - one could even consider it as an artifact of an inappropriate test - but at least for this dummy example, one can definitely say:

Yes, the JIT compiler (sometimes) eliminates redundant checks (for example, during method inlining)

There is only one null check, and one return instruction, returning -1, from the topmost method call.

One could now dig through the hoptspot code to find the optimization pass that actually does this compaction step, but the general (somewhat broad) answer is that the JIT is remarkably smart in many cases, and does eliminate checks that are "obviously" redundant.