static final fields vs TrustFinalNonStaticFields
Asked Answered
W

1

18

Suppose I have this simple method:

static final Integer me = Integer.parseInt("2");

static int go() {
    return me * 2;
}

For javac, me is not a constant (according to the JLS rules), but for JIT most probably is.

I tried to test this with:

 public class StaticFinal {

    public static void main(String[] args) {
        int hash = 0;
        for(int i=0;i<1000_000;++i){
            hash = hash ^ go();
        }
        System.out.println(hash);
    }

    static final Integer me = Integer.parseInt("2");

    static int go() {
        return me * 2;
    }
}

And running it with:

  java -XX:+UnlockDiagnosticVMOptions 
       -XX:-TieredCompilation  
       "-XX:CompileCommand=print,StaticFinal.go"  
       -XX:PrintAssemblyOptions=intel  
       StaticFinal.java

I do not know assembly very good, but this is obvious:

mov    eax,0x4

The result of go is immediately 4, i.e.: JIT "trusted" me to be a constant, thus 2 * 2 = 4.

If I drop static and change the code to:

public class NonStaticFinal {

    static NonStaticFinal instance = new NonStaticFinal();

    public static void main(String[] args) {
        int hash = 0;
        for(int i=0;i<1000_000;++i){
            hash = hash ^ instance.go();
        }
        System.out.println(hash);
    }

    final Integer me = Integer.parseInt("2");

    int go() {
        return me * 2;
    }
}

And run that with:

java -XX:+UnlockDiagnosticVMOptions 
     -XX:-TieredCompilation  
     "-XX:CompileCommand=print,NonStaticFinal.go"  
     -XX:PrintAssemblyOptions=intel  
     NonStaticFinal.java

I do see in assembly:

shl    eax,1

which is actually the multiplication of me with 2, done via a shift. So JIT did not trust me to be a constant, which is kind of expected.

And now the question. I thought that if I add TrustFinalNonStaticFields flag, I will see the same mov eax 0x4, i.e.: running with:

 java -XX:+UnlockDiagnosticVMOptions 
      -XX:-TieredCompilation  
      "-XX:CompileCommand=print,NonStaticFinal.go"  
      -XX:+UnlockExperimentalVMOptions 
      -XX:+TrustFinalNonStaticFields 
      -XX:PrintAssemblyOptions=intel  
      NonStaticFinal.java

should reveal mov eax,0x4, but to my surprise it does not, and the code stays as:

shl    eax,1

Can someone explain what is going on and what I am missing?

Wearisome answered 31/1, 2021 at 16:4 Comment(5)
By the way, the VM trusts non-static final fields in records and hidden classes, AFAIK. But you still need a "static final root".Novanovaculite
Johannes is right, about records as well. See ciField::trust_final_non_static_fields for the criteria of implicitly trusted final instance fields (notice how the if chain bottoms out in return TrustFinalNonStaticFields).Ketti
@JornVernee Some questions about the linked code: 1) Shouldn't it only check non-static final fields? jl.System does not have any instances - or instance members. 2) Is jl.String not in the java.lang package? 3) Are the box classes not in java.lang?Novanovaculite
@JohannesKuhn Both seem to be cruft (AFAICS); At the time the j.l.System exclude was added there were no instance fields in System either, and the check is only used for non-static fields. The java/lang include was added as part of the first memory-access API incubator in JDK 14. I don't know off the top of my head why java/lang was added, but any way, it seems the cleanup of the cases for j.l.String and the box classes was missed at that time.Ketti
Thanks @JornVernee. Was my suspicion, but I don't understand enough of the code to even suggest any action. So I just ask stupid questions, so I may at some point be able to do that.Novanovaculite
K
20

TrustFinalNonStaticFields enables folding of final instance fields from constant objects. In your example however, the instance field is non constant, so folding the load of the me field is not correct, since the instance object might still be changed at some point after compilation.

Furthermore, you're printing out the assembly for the go method, where this will not be seen as a constant if the method is compiled in isolation. To see the effect of TrustFinalNonStaticFields you need to look at the assembly for an inlined version of the go method, where the receiver is a constant. For instance:

 public class NonStaticFinal {

    static final NonStaticFinal instance = new NonStaticFinal();

    public static void main(String[] args) {
        for (int i = 0; i < 20_000; i++) { // trigger compilation of 'payload'
            payload();
        }
    }
    
    static int payload() {
        return instance.go();
    }

    final Integer me = Integer.parseInt("2");

    int go() {
        return me * 2;
    }

}

Running with:

java 
  -XX:+UnlockDiagnosticVMOptions 
  -XX:-TieredCompilation
  "-XX:CompileCommand=print,NonStaticFinal.payload"
  "-XX:CompileCommand=dontinline,NonStaticFinal.payload"
  -XX:+UnlockExperimentalVMOptions
  -XX:+TrustFinalNonStaticFields
  -XX:PrintAssemblyOptions=intel
  -Xbatch
  NonStaticFinal.java

Produces assembly where we can see the load + multiplication of the me field is being folded in the payload method:

  # {method} {0x0000016238c59470} 'payload' '()I' in 'NonStaticFinal'
  #           [sp+0x20]  (sp of caller)
  // set up frame
  0x00000162283d2500:   sub     rsp,18h
  0x00000162283d2507:   mov     qword ptr [rsp+10h],rbp     ;*synchronization entry
                                                            ; - NonStaticFinal::payload@-1 (line 12)
  // load a constant 4
  0x00000162283d250c:   mov     eax,4h     <-------------
  // clean up frame
  0x00000162283d2511:   add     rsp,10h
  0x00000162283d2515:   pop     rbp
  // safepoint poll
  0x00000162283d2516:   mov     r10,qword ptr [r15+110h]
  0x00000162283d251d:   test    dword ptr [r10],eax         ;   {poll_return}
  // return
  0x00000162283d2520:   ret

Compared to the version where TFNSF is disabled, where the load of the me field still occurs:

  # {method} {0x00000245f9669470} 'payload' '()I' in 'NonStaticFinal'
  #           [sp+0x20]  (sp of caller)
  // stack bang
  0x00000245e8d52a00:   mov     dword ptr [rsp+0ffffffffffff9000h],eax
  // set up frame
  0x00000245e8d52a07:   push    rbp
  0x00000245e8d52a08:   sub     rsp,10h                     ;*synchronization entry
                                                            ; - NonStaticFinal::payload@-1 (line 12)
  // load the 'instance' field. It's a constant, so the address here is constant
  0x00000245e8d52a0c:   mov     r10,70ff107a8h              ;   {oop(a 'NonStaticFinal'{0x000000070ff107a8})}
  // load the (compressed) oop 'me' field at 0ch (first field after the object header)
  0x00000245e8d52a16:   mov     r11d,dword ptr [r10+0ch]    ;*getfield me {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - NonStaticFinal::go@1 (line 18)
                                                            ; - NonStaticFinal::payload@3 (line 12)
  // Load the 'value' field from the Integer object.
  // r12 is the heap base, r11 the compressed oop 'Integer', *8 here to uncompress it,
  // and again loading the first field after the header at 0ch
  0x00000245e8d52a1a:   mov     eax,dword ptr [r12+r11*8+0ch]; implicit exception: dispatches to 0x00000245e8d52a31
  // multiply by 2
  // ABI returns ints in the 'eax' register, so no need to shuffle afterwards
  0x00000245e8d52a1f:   shl     eax,1h                      ;*imul {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - NonStaticFinal::go@8 (line 18)
                                                            ; - NonStaticFinal::payload@3 (line 12)
  // clean up frame
  0x00000245e8d52a21:   add     rsp,10h
  0x00000245e8d52a25:   pop     rbp
  // safepoint poll
  0x00000245e8d52a26:   mov     r10,qword ptr [r15+110h]
  0x00000245e8d52a2d:   test    dword ptr [r10],eax         ;   {poll_return}
  // return
  0x00000245e8d52a30:   ret
Ketti answered 31/1, 2021 at 16:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.