Do fields EVER get Hoisted and cached before thread scoping by Memory Models in OOP compilers?

132 views Asked by At

It seems to me OOP compilers may be ruled by different rules when inlining their code before simplification and hoisting.

At first, I thought that a full inlining process PLUS the hoisting process would force a hoist before the references enter the thread context.

static class Scope {
    private int field = 1;
    ScheduledExecutorService e = Executors.newSingleThreadScheduledExecutor();
    ScheduledExecutorService e2 = Executors.newSingleThreadScheduledExecutor();
    void write() {
        e.execute(
                () -> {
                    field = field + 3;
                    Printer.out.print("written...");
                    e.shutdown();
                }
        );
    }
    void read() {
        e2.schedule(
                () -> {
                    Printer.out.print(val() + "...read");
                    e2.shutdown();
                }
                ,
                5,
                TimeUnit.MILLISECONDS
        );
    }

    int val() {
        return field;
    }
}
public static void main(String[] args) {
    Printer.setAutoFlush(true); //If you use System you'll get delays, so use a flushable version.
    Scope scope = new Scope();
    scope.read();
    scope.write();
}

I am not particularly fluent in bytecode or processor/assembler instruction, but I will pseudocode what I believe the compiler may do...

At one point... a few steps before the code becomes completely inlined the runnable at read() may look like this.

() -> {
   Scope self = Scope.this;
   int cacheRead = self.val();
   Printer.out.print(cacheRead + "...read");
   e.shutdown();
}

Eventually the Printer class becomes unfolded, the read method itself until everything is inlined on the main() as a single sequence of instructions.

At which point, the compiler then begins to do inferences to help reduce interthread latencies, this means that every non-fenced read or write will be reordered and/or simplified.

The lines:

Scope self = Scope.this;
int cacheRead = self.field; //simplified to reference directly

Can then be simplified as:

int cacheRead = Scope.this.field;

This would mean that at this point, such a deep inlining process would make the compiler believe that what we did was a direct integer loading.

(BTW .val() will also be unfolded into bytecode, then C then assembler and processor instructions... in-between these translations the code crosses 2 reorderings, the JIT compiler reorder and the processor...)

The thing is that, since int is not volatile, the compiler MAY infer that the integer is NOT PART of any inter-process communication... so, to prevent latency it may hoist the value outside the Runnable instantiation (in this case inside it's scope...).

new Runnable() {
   int cacheRead = Scope.this.field;
   @Override
   public void run() {
      Printer.out.print(cacheRead + "...read");
      e.shutdown();
   }
}

Creating a memory visibility issue.

NOW... this is why I believe this MAY (WILL) NOT happen...

OOP syntax rules

If the inlining OBEYS the syntax and rules of the language... then the hoisting will NOT damage the visibility of the memory... EVEN IF there is an attempt at doing so:

   new Runnable() {
       Scope cachedScope = Scope.this;
       @Override
       public void run() {
          Printer.out.print(cachedScope.field + "...read"); //This may still be subjected to latency even if a hoisting occured.
          e.shutdown();
       }
    }

The implication here is that OOP compilers may not go into a deep inlining before going to the hoisting/caching step... (assuming all compilers steps are inlining -> simplification -> elimination -> hoisting in that order)

So if the programmer DOES NOT explicitly dereferences the primitive, the thing hoisted is the Object reference, not its separated fields.

I know that if multiple checks are done on the same non-volatile field... even if done via this. then the hoisting of the primitive occurs at the start when the first read is performed (in these cases opqueness/memory_oreder_relaxed is required).

The Java language is somewhat explicit in this sense... IF a lambda, captures a local value it forces us to make it final and dereference it before entering the lambda... IF the lambda captures a field, IT DOESN'T.

THIS to me is a didactic vehicle to help us remember the difference between locals and fields... so as to make EXPLICIT that fields will always force a shared register reading among all child scopes...

Of course... the Java language may have control over what their JIT compilers can do... but what about the processor optimization?

We know for a fact that memory latency cannot be influenced whatsoever by any language... this means that a field being non-volatile will never affect its "visibility" as a consequence of latency... all processors are cache coherent, so this is out of the question...

So, do fields EVER get hoisted and cached before thread scoping by Memory Models in OOP compilers?

What I know for a fact because of Java documentation is that VarHandle.getOpaque() will prevent a complete inlining... on primitives... the examples never disclose object referencing.

Not only will it prevent it, but the hoisting and simplification is completely prevented.

opaqueness according to the docs resemble the "readOnce" or "memory_order_relaxed"... the issue here is that the only thing we need to prevent is even lesser than the things it prevents... we just need to prevent complete hoisting on top, before runnable creation.

I know that if this code would have a double-check, then the integer... no matter if it is a field or local, would be hoisted on top at the first line inside the runnable (NOT before its creation) making a double check impossible and the compiler would simplify it and erase it, forcing a getOpaque on every read if we really want to allow for a double check.

But in this example only ONE read is performed, so no need to make it opaque... IF memory rules apply correctly.

0

There are 0 answers