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.
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.