web-dev-qa-db-fra.com

Java 8: Class.getName () ralentit la chaîne de concaténation des chaînes

Récemment, j'ai rencontré un problème concernant la concaténation de chaînes. Ce benchmark le résume:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BrokenConcatenationBenchmark {

  @Benchmark
  public String slow(Data data) {
    final Class<? extends Data> clazz = data.clazz;
    return "class " + clazz.getName();
  }

  @Benchmark
  public String fast(Data data) {
    final Class<? extends Data> clazz = data.clazz;
    final String clazzName = clazz.getName();
    return "class " + clazzName;
  }

  @State(Scope.Thread)
  public static class Data {
    final Class<? extends Data> clazz = getClass();

    @Setup
    public void setup() {
      //explicitly load name via native method Class.getName0()
      clazz.getName();
    }
  }
}

Sur JDK 1.8.0_222 (OpenJDK 64-Bit Server VM, 25.222-b10), j'ai les résultats suivants:

Benchmark                                                            Mode  Cnt     Score     Error   Units
BrokenConcatenationBenchmark.fast                                    avgt   25    22,253 ±   0,962   ns/op
BrokenConcatenationBenchmark.fast:·gc.alloc.rate                     avgt   25  9824,603 ± 400,088  MB/sec
BrokenConcatenationBenchmark.fast:·gc.alloc.rate.norm                avgt   25   240,000 ±   0,001    B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space            avgt   25  9824,162 ± 397,745  MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space.norm       avgt   25   239,994 ±   0,522    B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space        avgt   25     0,040 ±   0,011  MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space.norm   avgt   25     0,001 ±   0,001    B/op
BrokenConcatenationBenchmark.fast:·gc.count                          avgt   25  3798,000            counts
BrokenConcatenationBenchmark.fast:·gc.time                           avgt   25  2241,000                ms

BrokenConcatenationBenchmark.slow                                    avgt   25    54,316 ±   1,340   ns/op
BrokenConcatenationBenchmark.slow:·gc.alloc.rate                     avgt   25  8435,703 ± 198,587  MB/sec
BrokenConcatenationBenchmark.slow:·gc.alloc.rate.norm                avgt   25   504,000 ±   0,001    B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space            avgt   25  8434,983 ± 198,966  MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space.norm       avgt   25   503,958 ±   1,000    B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space        avgt   25     0,127 ±   0,011  MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space.norm   avgt   25     0,008 ±   0,001    B/op
BrokenConcatenationBenchmark.slow:·gc.count                          avgt   25  3789,000            counts
BrokenConcatenationBenchmark.slow:·gc.time                           avgt   25  2245,000                ms

Cela ressemble à un problème similaire à JDK-8043677 , où une expression ayant un effet secondaire rompt l'optimisation de la nouvelle chaîne StringBuilder.append().append().toString(). Mais le code de Class.getName() lui-même ne semble pas avoir d'effets secondaires:

private transient String name;

public String getName() {
  String name = this.name;
  if (name == null) {
    this.name = name = this.getName0();
  }

  return name;
}

private native String getName0();

La seule chose suspecte ici est un appel à une méthode native qui ne se produit en fait qu'une seule fois et son résultat est mis en cache dans le champ de la classe. Dans mon benchmark, je l'ai explicitement mis en cache dans la méthode de configuration.

Je m'attendais à ce que le prédicteur de branche comprenne qu'à chaque invocation de référence, la valeur réelle de this.name n'est jamais nulle et optimise l'expression entière.

Cependant, alors que pour la BrokenConcatenationBenchmark.fast() j'ai ceci:

@ 19   tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::fast (30 bytes)   force inline by CompileCommand
  @ 6   Java.lang.Class::getName (18 bytes)   inline (hot)
    @ 14   Java.lang.Class::initClassName (0 bytes)   native method
  @ 14   Java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
  @ 19   Java.lang.StringBuilder::append (8 bytes)   inline (hot)
  @ 23   Java.lang.StringBuilder::append (8 bytes)   inline (hot)
  @ 26   Java.lang.StringBuilder::toString (35 bytes)   inline (hot)

c'est-à-dire que le compilateur est capable de tout intégrer, pour BrokenConcatenationBenchmark.slow() c'est différent:

@ 19   tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::slow (28 bytes)   force inline by CompilerOracle
  @ 9   Java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
    @ 3   Java.lang.AbstractStringBuilder::<init> (12 bytes)   inline (hot)
      @ 1   Java.lang.Object::<init> (1 bytes)   inline (hot)
  @ 14   Java.lang.StringBuilder::append (8 bytes)   inline (hot)
    @ 2   Java.lang.AbstractStringBuilder::append (50 bytes)   inline (hot)
      @ 10   Java.lang.String::length (6 bytes)   inline (hot)
      @ 21   Java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   inline (hot)
        @ 17   Java.lang.AbstractStringBuilder::newCapacity (39 bytes)   inline (hot)
        @ 20   Java.util.Arrays::copyOf (19 bytes)   inline (hot)
          @ 11   Java.lang.Math::min (11 bytes)   (intrinsic)
          @ 14   Java.lang.System::arraycopy (0 bytes)   (intrinsic)
      @ 35   Java.lang.String::getChars (62 bytes)   inline (hot)
        @ 58   Java.lang.System::arraycopy (0 bytes)   (intrinsic)
  @ 18   Java.lang.Class::getName (21 bytes)   inline (hot)
    @ 11   Java.lang.Class::getName0 (0 bytes)   native method
  @ 21   Java.lang.StringBuilder::append (8 bytes)   inline (hot)
    @ 2   Java.lang.AbstractStringBuilder::append (50 bytes)   inline (hot)
      @ 10   Java.lang.String::length (6 bytes)   inline (hot)
      @ 21   Java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   inline (hot)
        @ 17   Java.lang.AbstractStringBuilder::newCapacity (39 bytes)   inline (hot)
        @ 20   Java.util.Arrays::copyOf (19 bytes)   inline (hot)
          @ 11   Java.lang.Math::min (11 bytes)   (intrinsic)
          @ 14   Java.lang.System::arraycopy (0 bytes)   (intrinsic)
      @ 35   Java.lang.String::getChars (62 bytes)   inline (hot)
        @ 58   Java.lang.System::arraycopy (0 bytes)   (intrinsic)
  @ 24   Java.lang.StringBuilder::toString (17 bytes)   inline (hot)

La question est donc de savoir si c'est le comportement approprié de la JVM ou du bogue du compilateur?

Je pose la question parce que certains projets utilisent toujours Java 8 et si cela ne sera pas corrigé sur les mises à jour de versions, alors pour moi, il est raisonnable de lever les appels à la fonction Class.getName() manuellement à partir des points chauds.

P.S. Sur les derniers JDK (11, 13, 14 eap), le problème n'est pas reproduit.

13
Sergey Tsypanov

Légèrement sans rapport mais depuis Java 9 et JEP 280: Indify String Concatenation la concaténation des chaînes se fait maintenant avec invokedynamic et non StringBuilder . Cet article montre les différences dans le bytecode entre Java 8 et Java 9.

Si le benchmark est relancé sur une version plus récente Java ne montre pas le problème, il n'y a probablement pas de bogue dans javac car le compilateur utilise maintenant un nouveau mécanisme. Je ne sais pas si la plongée into Java 8 est bénéfique s'il y a un tel changement substantiel dans les versions plus récentes.

1
Karol Dowbecki