Do modern JIT compilers keep Program Order(PO) inside spinning loops?

54 views Asked by At

The consensus around here seems to be that the famous example given in the "Java Concurrency in Practice" book:

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    } 
}

Can have 2 types of errors:

  • Either ReaderThread prints 0 or
  • ReaderThread gets stuck in an infinite loop

The fact of the matter is that Processors give NO ACTUAL access to alter latency speeds of their Cache Coherency(TM) protocols... so... all volatile does here is reordering...

Which means that:

  • Not only "print 0" is because of reordering... but ALSO...
  • The less obvious "stuck in an infinite loop" issue is NOT because of Cache
    Coherency(TM).. is because of reordering... to be precise a
    combination of 2 reorderings... the JIT reordering PLUS the processor runtime reordering.

So... after reading this it seems to me the most logical way in which JIT compilers and processors OBEY volatile non-reordering semantics, is by keeping their instruction... position at place.

If a program order branch is composed of 200 instructions and a volatile write happens at instruction #34...everything above it and below it can be reordered... omitted... and even completely erased if redundancies are found, BUT the position of the volatile instruction "write" is kept in the exact same place...

Nothing that was written ABOVE it can go BELLOW it, and vice versa.

So how is the ReaderThread failing? According to this 2 sources: video at 25:33 and this SO answer

The compiler is looking at the Thread as a single unit to optimize... and if the value loaded inside the spin-lock is NOT changed through out the lifecycle of the spin, then the value is dereferenced from it's original register... and this is the thing that is preventing the Thread to read a proper value... because is reading a different register...

The issue here is that the compiler (JIT in this case) has a lot of freedom to optimize code. For example if it detects that the same field is read in a loop, it could decide to hoist that variable out of the loop as is shown below.

for(...){ int tmp = a; println(tmp); }

After hoisting:

int tmp = a; for(...){ println(tmp); }

This is the thing... the Java Memory Model Pragmatics (transcript) states that the Thread code's PO is seen as independent from the entirety of the program...

Here is where Program Order (PO) jumps in. To filter out the executions we can take to reason about the particular program, we have intra-thread consistency rules, which eliminate all unrelated executions.

So, the optimization decides to do this because the WHOLE is not being seen, so from the perspective of the Thread nothing will ever change the captured reference.

In the VarHandle video, the solution is using getOpaque inside the spin... and this opens more questions... getOpaque does nothing it would be like reading the variable as being a normal variable without "volatile" keyword... This makes absolute sense since Cache Coherency(TM) is not the issue... processors handle this immediately.

This means that the getOpaque solution is done for backwards compatibility... so that older JIT compilers see the getOpaque as a "volatile" and not damage the spinlock.

So there are 2 options I see modern JIT compilers are doing.

  • Before intra-thread reordering, a previous inference is done on the WHOLE program, catching on potential reference changes... OR...

  • Spinlock instructions are now reorganized/reordered from the loads happening inside it.

It seems because C and C++ never had these issues... as opposed to C#... that the reordering made by the Processor never destroyed these spinlocks...

So, my question is, Which strategy, are modern JIT compilers doing to not destroy the spinlock?

If I am wrong about something you are welcomed to correct it, thank you beforehand.

0

There are 0 answers