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 StringBuilder.append().append().toString()
chaîne. Mais le code en 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 le 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, car 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 s'il s'agit du comportement approprié de la JVM ou du bogue du compilateur?
Je pose la question parce que certains des projets utilisent toujours Java 8 et si cela ne sera pas résolu sur aucune des mises à jour de version, alors pour moi, il est raisonnable de hisser les appels vers Class.getName()
manuellement depuis les points chauds.
PS Sur les derniers JDK (11, 13, 14 eap), le problème n'est pas reproduit.
la source
this.name
.Class.getName()
et dans lasetUp()
méthode, et non dans le corps de la référence.Réponses:
HotSpot JVM collecte des statistiques d'exécution par bytecode. Si le même code est exécuté dans différents contextes, le profil de résultat agrégera les statistiques de tous les contextes. Cet effet est connu sous le nom de pollution de profil .
Class.getName()
est évidemment appelé non seulement à partir de votre code de référence. Avant que JIT ne commence à compiler le benchmark, il sait déjà que la condition suivante aClass.getName()
été remplie plusieurs fois:Au moins, suffisamment de fois pour traiter cette branche statistiquement importante. Ainsi, JIT n'a pas exclu cette branche de la compilation et n'a donc pas pu optimiser la concaténation des chaînes en raison d'un effet secondaire possible.
Cela n'a même pas besoin d'être un appel de méthode native. Une simple affectation régulière sur le terrain est également considérée comme un effet secondaire.
Voici un exemple de la façon dont la pollution des profils peut nuire à d'autres optimisations.
Il s'agit essentiellement de la version modifiée de votre benchmark qui simule la pollution du
getName()
profil. Selon le nombre d'getName()
appels préliminaires sur un nouvel objet, les performances supplémentaires de la concaténation de chaînes peuvent différer considérablement:Plus d'exemples de pollution de profil »
Je ne peux pas appeler cela un bug ou un "comportement approprié". C'est ainsi que la compilation adaptative dynamique est implémentée dans HotSpot.
la source
Légèrement sans rapport mais depuis Java 9 et JEP 280: Indiquez la concaténation de chaînes, la concaténation de chaînes est maintenant effectuée avec
invokedynamic
et nonStringBuilder
. Cet article montre les différences de bytecode entre Java 8 et Java 9.Si le test de référence sur une nouvelle version de Java ne montre pas le problème, il n'y a probablement pas de bogue
javac
car le compilateur utilise maintenant un nouveau mécanisme. Je ne sais pas si plonger dans le comportement de Java 8 est bénéfique s'il y a un tel changement substantiel dans les nouvelles versions.la source
javac
bien.javac
génère du bytecode et ne fait aucune optimisation sophistiquée. J'ai exécuté le même benchmark avec-XX:TieredStopAtLevel=1
et reçu cette sortie:Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 74,677 ? 2,961 ns/op
BrokenConcatenationBenchmark.slow avgt 25 69,316 ? 1,239 ns/op
Donc, lorsque nous n'optimisons pas beaucoup les deux méthodes produisent les mêmes résultats, le problème ne se révèle que lorsque le code est compilé en C2.invokedynamic
indique seulement au runtime de choisir comment faire la concaténation, et 5 stratégies sur 6 (y compris la stratégie par défaut) utilisent toujoursStringBuilder
.StringConcatFactory.Strategy
enum?