It's All Relative

Performance Minded

09 Oct 2012

6 mins read

Volatile

Volatile Java keyword is already explained in many places around the web (like in What volatile Means in Java for example). So in this post I will not explain it once again, instead, I will focus on what it means in terms of JIT compiler native code production.

Safety tips: Do try this at home ! ;-)

So let’s take a classic example of boolean variable used to go out of a thread loop:

 public class TestJIT
{
    public static void main(String[] args) throws Exception
    {
        C c = new C();
        c.start();
        Thread.sleep(1*1000);
        c.stopNow();
        c.join();
    }
    
    private static class C extends Thread
    {
        private boolean running = true;
        private int i;
        
        @Override
        public void run()
        {
            while (running)
            {
                i++;
            }
            if (System.currentTimeMillis() == 0)
            {
                System.out.println(i);
            }
        }
        
        public void stopNow()
        {
            running = false;
        }
    }
}

Basically in this code, I start a thread, wait for 1 second and then notify the thread to stop by changing the running boolean to false.

If you compile and execute (in -server on windows x86 in my case) this code, you will observe that it never ends !

Easy, you think ! It’s because boolean is not volatile and the thread loop does not see the variable changing because it is not yet read from the main memory.

Ok makes sense, let’s see by ourself by disassembling the JIT code (I will explain how to do this in a next post):

[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
  # {method} 'run' '()V' in 'TestJIT$C'
  0x02608ec0: int3 
  0x02608ec1: xchg   %ax,%ax
  0x02608ec4: mov    %eax,0xffffd000(%esp)
  0x02608ecb: push   %ebp
  0x02608ecc: sub    $0x18,%esp
  0x02608ed2: mov    (%ecx),%ebp
  0x02608ed4: mov    %ecx,(%esp)
  0x02608ed7: call   0x6dbadeb0         ;   {runtime_call}
  0x02608edc: mov    0x4(%ebp),%ecx     ; implicit exception: disp. to x02608f1d
  0x02608edf: cmp    $0x576e470,%ecx    ;   {oop('TestJIT$C')}
  0x02608ee5: jne    0x02608f11         ;*aload_0
                                        ; - TestJIT$C::run@3 (line 24)
  0x02608ee7: movzbl 0x70(%ebp),%ecx    ;*getfield running
                                        ; - TestJIT$C::run@14 (line 22)
  0x02608eeb: mov    $0x1,%edi
  0x02608ef0: add    0x6c(%ebp),%edi    ;*iadd
                                        ; - TestJIT$C::run@9 (line 24)
  0x02608ef3: mov    %edi,0x6c(%ebp)    ;*putfield i
                                        ; - TestJIT$C::run@10 (line 24)
  0x02608ef6: test   %ecx,%ecx
  0x02608ef8: je     0x02608f06         ; OopMap{ebp=Oop off=58}
                                        ;*ifne
                                        ; - TestJIT$C::run@17 (line 22)
  0x02608efa: test   %edi,0x110000      ;*ifne
                                        ; - TestJIT$C::run@17 (line 22)
                                        ;   {poll}
  0x02608f00: inc    %edi               ;*iadd
                                        ; - TestJIT$C::run@9 (line 24)
  0x02608f01: mov    %edi,0x6c(%ebp)    ;*putfield i
                                        ; - TestJIT$C::run@10 (line 24)
  0x02608f04: jmp    0x02608efa
  0x02608f06: mov    $0xffffff86,%ecx
  0x02608f0b: call   0x025ec700         ; OopMap{ebp=Oop off=80}
                                        ;*aload_0
                                        ; - TestJIT$C::run@13 (line 22)
                                        ;   {runtime_call}
  0x02608f10: int3 
  0x02608f11: mov    $0xffffffad,%ecx
  0x02608f16: nop  
  0x02608f17: call   0x025ec700         ; OopMap{ebp=Oop off=92}
                                        ;*aload_0
                                        ; - TestJIT$C::run@3 (line 24)
                                        ;   {runtime_call}
  0x02608f1c: int3                      ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)
  0x02608f1d: mov    $0xfffffff6,%ecx
  0x02608f22: nop  
  0x02608f23: call   0x025ec700         ; OopMap{off=104}
                                        ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)
                                        ;   {runtime_call}
  0x02608f28: int3                      ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)

So here, you see the loop:

  0x02608efa: test   %edi,0x110000      ;*ifne
                                        ; - TestJIT$C::run@17 (line 22)
                                        ;   {poll}
  0x02608f00: inc    %edi               ;*iadd
                                        ; - TestJIT$C::run@9 (line 24)
  0x02608f01: mov    %edi,0x6c(%ebp)    ;*putfield i
                                        ; - TestJIT$C::run@10 (line 24)
  0x02608f04: jmp    0x02608efa

There is an unconditional jump (jmp instruction). It means that the JIT produced in fact an infinite loop as a while (true) i++;

There is also no emitted code for the rest of the run method meaning that the test on System.currentTimeMillis & and the System.out.println lines were optimized away since they are now unreachable.

So let’s add the volatile keyword to our running boolean and see what happens. Firstly the code now works. Secondly, look at the generated code:

[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
  # {method} 'run' '()V' in 'TestJIT$C'
  0x026d8f00: int3 
  0x026d8f01: xchg   %ax,%ax
  0x026d8f04: mov    %eax,0xffffd000(%esp)
  0x026d8f0b: push   %ebp
  0x026d8f0c: sub    $0x18,%esp
  0x026d8f12: mov    (%ecx),%ebp
  0x026d8f14: mov    %ecx,(%esp)
  0x026d8f17: call   0x6dbadeb0         ;   {runtime_call}
  0x026d8f1c: mov    0x4(%ebp),%ecx     ; implicit exception: disp. to 0x026d8f5d
  0x026d8f1f: cmp    $0x583e470,%ecx    ;   {oop('TestJIT$C')}
  0x026d8f25: jne    0x026d8f51         ;*aload_0
                                        ; - TestJIT$C::run@3 (line 24)
  0x026d8f27: incl   0x6c(%ebp)         ;*putfield i
                                        ; - TestJIT$C::run@10 (line 24)
  0x026d8f2a: jmp    0x026d8f37
  0x026d8f2c: nopl   0x0(%eax)
  0x026d8f30: mov    0x6c(%ebp),%ecx    ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)
  0x026d8f33: inc    %ecx
  0x026d8f34: mov    %ecx,0x6c(%ebp)    ;*aload_0
                                        ; - TestJIT$C::run@13 (line 22)
  0x026d8f37: movzbl 0x70(%ebp),%ebx    ; OopMap{ebp=Oop off=59}
                                        ;*ifne
                                        ; - TestJIT$C::run@17 (line 22)
  0x026d8f3b: test   %edi,0x180000      ;   {poll}
  0x026d8f41: test   %ebx,%ebx
  0x026d8f43: jne    0x026d8f30         ;*ifne
                                        ; - TestJIT$C::run@17 (line 22)
  0x026d8f45: mov    $0x18,%ecx
  0x026d8f4a: nop  
  0x026d8f4b: call   0x026bc700         ; OopMap{ebp=Oop off=80}
                                        ;*invokestatic currentTimeMillis
                                        ; - TestJIT$C::run@20 (line 26)
                                        ;   {runtime_call}
  0x026d8f50: int3 
  0x026d8f51: mov    $0xffffffad,%ecx
  0x026d8f56: nop  
  0x026d8f57: call   0x026bc700         ; OopMap{ebp=Oop off=92}
                                        ;*aload_0
                                        ; - TestJIT$C::run@3 (line 24)
                                        ;   {runtime_call}
  0x026d8f5c: int3                      ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)
  0x026d8f5d: mov    $0xfffffff6,%ecx
  0x026d8f62: nop  
  0x026d8f63: call   0x026bc700         ; OopMap{off=104}
                                        ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)
                                        ;   {runtime_call}
  0x026d8f68: int3                      ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)

Now we have something very different in the loop:

  0x026d8f30: mov    0x6c(%ebp),%ecx    ;*getfield i
                                        ; - TestJIT$C::run@5 (line 24)
  0x026d8f33: inc    %ecx
  0x026d8f34: mov    %ecx,0x6c(%ebp)    ;*aload_0
                                        ; - TestJIT$C::run@13 (line 22)
  0x026d8f37: movzbl 0x70(%ebp),%ebx    ; OopMap{ebp=Oop off=59}
                                        ;*ifne
                                        ; - TestJIT$C::run@17 (line 22)
  0x026d8f3b: test   %edi,0x180000      ;   {poll}
  0x026d8f41: test   %ebx,%ebx
  0x026d8f43: jne    0x026d8f30         ;*ifne
                                        ; - TestJIT$C::run@17 (line 22)

We get the field i from memory into ecx register, we increment ecx, we move the result to the previous location, we load the running field from the memory into ebx register, and we test if it equals to 0, if not we jump to the first instruction of the loop.

In the comment you see mentioning a call to currentTimeMillis, in fact, there is the same address for the 2 next calls. Since my loop was compiled with OSR optimization, (you can check with -XX:+PrintCompilation) this is a call to C2I adapter (Compiler to Interpreter) that is emitted to go back to interpreter mode for the rest of the method since the whole method is not yet considered as a hot spot.

Note: movzbl is basically a move with zero padding.

So in conclusion, is there a bug into the server JIT (C2) ? I do no think so ! Without volatile keyword, C2 is free to optimize optimisticly (aggressively ?) the code since you do not put hard guarantee on your boolean as specified in the Memory Model of the JLS.