Scalar replacement issue in JDK 14.0.1

Vladimir Ivanov vladimir.x.ivanov at oracle.com
Tue Jun 30 13:48:28 UTC 2020


Hi Sergey,

I took a look at the benchmark and I think there's more than 11 vs 14 in 
play here.

When I compiled the benchmark with jdk8, I saw the following in the 
compilation log (irrespective of jdk version used):

    1221   67    b 
org.openjdk.ea.StringCompositeKeyBenchmark::compositeKey (22 bytes)
...
         @ 15   org.openjdk.ea.StringCompositeKeyBenchmark$Key::<init> 
(7 bytes)   unloaded signature classes
...

The constructor is not inlined, so even if the Key instance doesn't 
escape globally, it escapes into a call and C2 can't scalar replace it.

The reason why inlining fails is private constructor can't be accessed 
directly, but requires a bridge method. Bridge method has additional 
method argument which has a non-existent type (with unique name). 
Inlining heuristics don't inline methods which have unresolved classes 
in their signatures.

But if you recompile the benchmark with jdk11 (or later), inlining 
happends and the allocation is eliminated [1].

If you look at the bytecodes, there's no bridge method anymore. Javac 
generates NestMembers attribute instead which is enough to make private 
constructor accessible from the enclosing class:

$ javap -verbose -private 
target/classes//org/openjdk/ea/StringCompositeKeyBenchmark.class
...
NestMembers:
   org/openjdk/ea/StringCompositeKeyBenchmark$Key
   org/openjdk/ea/StringCompositeKeyBenchmark$Data
...

So, it boils down to the target language level being used. Starting 11, 
javac doesn't emit bridge methods anymore and it helps with getting EA 
in C2 to eliminate the allocation.

Best regards,
Vladimir Ivanov

[1]

$ javap -verbose -private 
target/classes//org/openjdk/ea/StringCompositeKeyBenchmark.class

   public java.lang.Object 
compositeKey(org.openjdk.ea.StringCompositeKeyBenchmark$Data);
     descriptor: 
(Lorg/openjdk/ea/StringCompositeKeyBenchmark$Data;)Ljava/lang/Object;
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=6, locals=2, args_size=2
          0: aload_1
          1: invokestatic  #9                  // Method 
org/openjdk/ea/StringCompositeKeyBenchmark$Data.access$200:(Lorg/openjdk/ea/StringCompositeKeyBenchmark$Data;)Ljava/util/HashMap;
          4: new           #13                 // class 
org/openjdk/ea/StringCompositeKeyBenchmark$Key
          7: dup
          8: ldc           #15                 // String code1
         10: aload_1
         11: invokestatic  #17                 // Method 
org/openjdk/ea/StringCompositeKeyBenchmark$Data.access$000:(Lorg/openjdk/ea/StringCompositeKeyBenchmark$Data;)Ljava/util/Locale;
         14: aconst_null
         15: invokespecial #21                 // Method 
org/openjdk/ea/StringCompositeKeyBenchmark$Key."<init>":(Ljava/lang/String;Ljava/util/Locale;Lorg/openjdk/ea/StringCompositeKeyBenchmark$1;)V
         18: invokevirtual #24                 // Method 
java/util/HashMap.get:(Ljava/lang/Object;)Ljava/lang/Object;
         21: areturn

$ javap -verbose -private 
target/classes//org/openjdk/ea/StringCompositeKeyBenchmark\$Key.class
...
   org.openjdk.ea.StringCompositeKeyBenchmark$Key(java.lang.String, 
java.util.Locale, org.openjdk.ea.StringCompositeKeyBenchmark$1);
     descriptor: 
(Ljava/lang/String;Ljava/util/Locale;Lorg/openjdk/ea/StringCompositeKeyBenchmark$1;)V
...

[2]
    1426   67    b 
org.openjdk.ea.StringCompositeKeyBenchmark::compositeKey (26 bytes)

======== Connection graph for 
org.openjdk.ea.StringCompositeKeyBenchmark::compositeKey
JavaObject NoEscape(NoEscape) [ 148F 142F 144F 137F [ 58 63 ]]   46 
Allocate	===  29  6  7  8  1 ( 44  43  39  1  1  37  42 ) [[ 47  48  49 
56  57  58 ]]  rawptr:NotNull ( int:>=0, java/lang/Object:NotNull *, 
bool, top ) StringCompositeKeyBenchmark::compositeKey @ bci:4 !jvms: 
StringCompositeKeyBenchmark::compositeKey @ bci:4
LocalVar [ 46P [ 63 148b 142b ]]   58	Proj	===  46  [[ 59  63  142  148 
]] #5 !jvms: StringCompositeKeyBenchmark::compositeKey @ bci:4
LocalVar [ 58 46P [ 144b 137b ]]   63	CheckCastPP	===  60  58  [[ 906 
865  814  814  865  717  689  689  144  678  717  437  137  137  144 
155  166  185  678  678  649  450  223  241  649  634  618  598  576 
522  522  990  990  478  478  450  371  371  424  424  437 ]] 
#org/openjdk/ea/StringCompositeKeyBenchmark$Key:NotNull:exact * 
Oop:org/openjdk/ea/StringCompositeKeyBenchmark$Key:NotNull:exact * 
!jvms: StringCompositeKeyBenchmark::compositeKey @ bci:4

Scalar  63	CheckCastPP	===  60  58  [[ 906  865  814  814  865  717  689 
  689  424  990  717  437  522  522  424  437  166  185  990  478  478 
450  223  241  450  634  618  598  576 ]] 
#org/openjdk/ea/StringCompositeKeyBenchmark$Key:NotNull:exact *,iid=46 
Oop:org/openjdk/ea/StringCompositeKeyBenchmark$Key:NotNull:exact 
*,iid=46 !jvms: StringCompositeKeyBenchmark::compositeKey @ bci:4
++++ Eliminated: 46 Allocate

                             @ 9   java.util.Objects::requireNonNull (14 
bytes)   inline (hot)
                             @ 19 
org.openjdk.ea.StringCompositeKeyBenchmark$Key::<init> (15 bytes) 
inline (hot)
                               @ 1   java.lang.Object::<init> (1 bytes) 
  inline (hot)



On 26.06.2020 15:06, Сергей Цыпанов wrote:
> Hello,
> 
> while looking into an issue I've found out that scalar replacement is not working in trivial case on JDK 14.0.1.
> 
> This benchmark illustrates the issue:
> 
> @State(Scope.Thread)
> @BenchmarkMode(Mode.AverageTime)
> @OutputTimeUnit(TimeUnit.NANOSECONDS)
> @Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
> public class StringCompositeKeyBenchmark {
>    @Benchmark
>    public Object compositeKey(Data data) {
>      return data.keyObjectMap.get(new Key(data.code, data.locale));
>    }
> 
> 
>    @State(Scope.Thread)
>    public static class Data {
>      private final String code = "code1";
>      private final Locale locale = Locale.getDefault();
> 
>      private final HashMap<Key, Object> keyObjectMap = new HashMap<>();
> 
>      @Setup
>      public void setUp() {
>        keyObjectMap.put(new Key(code, locale), new Object());
>      }
>    }
> 
>    private static final class Key {
>      private final String code;
>      private final Locale locale;
> 
>      private Key(String code, Locale locale) {
>        this.code = code;
>        this.locale = locale;
>      }
> 
>      @Override
>      public boolean equals(Object o) {
>        if (this == o) return true;
>        if (o == null || getClass() != o.getClass()) return false;
> 
>        Key key = (Key) o;
> 
>        if (!code.equals(key.code)) return false;
>        return locale.equals(key.locale);
>      }
> 
>      @Override
>      public int hashCode() {
>        return 31 * code.hashCode() + locale.hashCode();
>      }
>    }
> }
> 
> When I run this on JDK 11 (JDK 11.0.7, OpenJDK 64-Bit Server VM, 11.0.7+10-post-Ubuntu-2ubuntu218.04) I get this output:
> 
> Benchmark                                                     Mode  Cnt   Score    Error   Units
> StringCompositeKeyBenchmark.compositeKey                      avgt   10   5.510 ±  0.121   ns/op
> StringCompositeKeyBenchmark.compositeKey:·gc.alloc.rate       avgt   10  ≈ 10⁻⁴           MB/sec
> StringCompositeKeyBenchmark.compositeKey:·gc.alloc.rate.norm  avgt   10  ≈ 10⁻⁶             B/op
> StringCompositeKeyBenchmark.compositeKey:·gc.count            avgt   10     ≈ 0           counts
> 
> As I understand Java runtime erases object allocation here and we don't use additional memory.
> 
> Same run on JDK 14 (JDK 14.0.1, Java HotSpot(TM) 64-Bit Server VM, 14.0.1+7) demonstrate object allocation per each method call:
> 
> Benchmark                                                                  Mode  Cnt     Score     Error   Units
> StringCompositeKeyBenchmark.compositeKey                                   avgt   10     7.958 ±   1.360   ns/op
> StringCompositeKeyBenchmark.compositeKey:·gc.alloc.rate                    avgt   10  1937.551 ± 320.718  MB/sec
> StringCompositeKeyBenchmark.compositeKey:·gc.alloc.rate.norm               avgt   10    24.001 ±   0.001    B/op
> StringCompositeKeyBenchmark.compositeKey:·gc.churn.G1_Eden_Space           avgt   10  1879.111 ± 596.770  MB/sec
> StringCompositeKeyBenchmark.compositeKey:·gc.churn.G1_Eden_Space.norm      avgt   10    23.244 ±   5.509    B/op
> StringCompositeKeyBenchmark.compositeKey:·gc.churn.G1_Survivor_Space       avgt   10     0.267 ±   0.750  MB/sec
> StringCompositeKeyBenchmark.compositeKey:·gc.churn.G1_Survivor_Space.norm  avgt   10     0.003 ±   0.009    B/op
> StringCompositeKeyBenchmark.compositeKey:·gc.count                         avgt   10    23.000            counts
> StringCompositeKeyBenchmark.compositeKey:·gc.time                          avgt   10    44.000                ms
> 
> At the same time in more trivial scenario like
> 
> @Benchmark
> public int compositeKey(Data data) {
>    return new Key(data.code, data.locale).hashCode();
> }
> 
> scalar replacement again eliminates allocation of object.
> 
> So I'm curious whether this is normal behaviour or a bug?
> 
> Regards,
> Sergey Tsypanov
> 


More information about the core-libs-dev mailing list