web-dev-qa-db-fra.com

Pourquoi StringBuilder # append (int) est-il plus rapide dans Java 7 que dans Java 8?)

En enquêtant pour un petit débat w.r.t. en utilisant "" + n et Integer.toString(int) pour convertir une primitive entière en une chaîne, j'ai écrit ceci JMH = microbenchmark:

@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
    protected int counter;


    @GenerateMicroBenchmark
    public String integerToString() {
        return Integer.toString(this.counter++);
    }

    @GenerateMicroBenchmark
    public String stringBuilder0() {
        return new StringBuilder().append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder1() {
        return new StringBuilder().append("").append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder2() {
        return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
    }

    @GenerateMicroBenchmark
    public String stringFormat() {
        return String.format("%d", this.counter++);
    }

    @Setup(Level.Iteration)
    public void prepareIteration() {
        this.counter = 0;
    }
}

Je l'ai exécuté avec les options JMH par défaut avec les deux Java qui existent sur ma machine Linux (Mageia 4 64 bits à jour, processeur Intel i7-3770, 32 Go de RAM). la première JVM était celle fournie avec Oracle JDK 8u5 64 bits:

Java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

Avec cette machine virtuelle Java, j'ai obtenu à peu près ce que j'attendais:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32317.048      698.703   ops/ms
b.IntStr.stringBuilder0     thrpt        20    28129.499      421.520   ops/ms
b.IntStr.stringBuilder1     thrpt        20    28106.692     1117.958   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20066.939     1052.937   ops/ms
b.IntStr.stringFormat       thrpt        20     2346.452       37.422   ops/ms

C'est à dire. l'utilisation de la classe StringBuilder est plus lente en raison de la surcharge supplémentaire liée à la création de l'objet StringBuilder et à l'ajout d'une chaîne vide. L'utilisation de String.format(String, ...) est encore plus lente, d'un ordre de grandeur environ.

Le compilateur fourni par la distribution, d'autre part, est basé sur OpenJDK 1.7:

Java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)

Les résultats ici étaient intéressants:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    31249.306      881.125   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39486.857      663.766   ops/ms
b.IntStr.stringBuilder1     thrpt        20    41072.058      484.353   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20513.913      466.130   ops/ms
b.IntStr.stringFormat       thrpt        20     2068.471       44.964   ops/ms

Pourquoi StringBuilder.append(int) apparaît-il beaucoup plus rapidement avec cette machine virtuelle Java? L'examen du code source de la classe StringBuilder n'a rien révélé de particulièrement intéressant - la méthode en question est presque identique à Integer#toString(int). Il est intéressant de noter que l'ajout du résultat de Integer.toString(int) (la microbenchmark stringBuilder2) Ne semble pas être plus rapide.

Cette différence de performances est-elle un problème avec le faisceau de test? Ou est-ce que ma machine virtuelle Java OpenJDK contient des optimisations qui affecteraient ce code (anti) modèle particulier?

MODIFIER:

Pour une comparaison plus simple, j'ai installé Oracle JDK 1.7u55:

Java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

Les résultats sont similaires à ceux d'OpenJDK:

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32502.493      501.928   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39592.174      428.967   ops/ms
b.IntStr.stringBuilder1     thrpt        20    40978.633      544.236   ops/ms

Il semble que ce soit un problème plus général Java 7 vs Java 8 problème. Peut-être Java 7 avait des optimisations de chaînes plus agressives) ?

EDIT 2 :

Pour être complet, voici les options liées à la chaîne VM pour ces deux machines virtuelles Java:

Pour Oracle JDK 8u5:

$ /usr/Java/default/bin/Java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}
     intx PerfMaxStringConstLength                  = 1024            {product}
     bool PrintStringTableStatistics                = false           {product}
    uintx StringTableSize                           = 60013           {product}

Pour OpenJDK 1.7:

$ Java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}        
     intx PerfMaxStringConstLength                  = 1024            {product}           
     bool PrintStringTableStatistics                = false           {product}           
    uintx StringTableSize                           = 60013           {product}           
     bool UseStringCache                            = false           {product}   

L'option UseStringCache a été supprimée dans Java 8 sans remplacement, donc je doute que cela fasse une différence. Les autres options semblent avoir les mêmes paramètres.

EDIT 3:

Une comparaison côte à côte du code source des classes AbstractStringBuilder, StringBuilder et Integer du fichier src.Zip De ne révèle rien de remarquable. Mis à part de nombreux changements cosmétiques et de documentation, Integer prend désormais en charge les entiers non signés et StringBuilder a été légèrement refactorisé pour partager plus de code avec StringBuffer. Aucune de ces modifications ne semble affecter les chemins de code utilisés par StringBuilder#append(int), même si j'ai peut-être manqué quelque chose.

Une comparaison du code Assembly généré pour IntStr#integerToString() et IntStr#stringBuilder0() est beaucoup plus intéressante. La disposition de base du code généré pour IntStr#integerToString() était similaire pour les deux machines virtuelles Java, bien qu'Oracle JDK 8u5 semble être plus agressif avec w.r.t. en insérant certains appels dans le code Integer#toString(int). Il y avait une correspondance claire avec le code source Java, même pour quelqu'un avec une expérience d'assemblage minimale.

Le code d'assembly pour IntStr#stringBuilder0(), cependant, était radicalement différent. Le code généré par Oracle JDK 8u5 était encore une fois directement lié au code source Java - je pouvais facilement reconnaître la même disposition. Au contraire, le code généré par OpenJDK 7 était presque méconnaissable par le oeil inexpérimenté (comme le mien). L'appel new StringBuilder() a apparemment été supprimé, tout comme la création du tableau dans le constructeur StringBuilder. De plus, le plugin désassembleur n'a pas pu fournir autant de références au code source comme il l'a fait dans JDK 8.

Je suppose que c'est soit le résultat d'une passe d'optimisation beaucoup plus agressive dans OpenJDK 7, soit plus probablement le résultat de l'insertion de code bas niveau manuscrit pour certaines opérations StringBuilder. Je ne sais pas pourquoi cette optimisation ne se produit pas dans mon implémentation JVM 8 ou pourquoi les mêmes optimisations n'ont pas été implémentées pour Integer#toString(int) dans JVM 7. Je suppose qu'une personne familière avec les parties connexes du code source JRE devrait répondez à ces questions...

76
thkala

TL; DR: Les effets secondaires dans append cassent apparemment les optimisations de StringConcat.

Très bonne analyse dans la question d'origine et les mises à jour!

Pour être complet, voici quelques étapes manquantes:

  • Voir à travers le -XX:+PrintInlining pour 7u55 et 8u5. En 7u55, vous verrez quelque chose comme ceci:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   Java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
       @ 18   Java.lang.StringBuilder::append (8 bytes)   already compiled into a big method
       @ 21   Java.lang.StringBuilder::toString (17 bytes)   inline (hot)
    

    ... et en 8u5:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   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)
       @ 18   Java.lang.StringBuilder::append (8 bytes)   inline (hot)
         @ 2   Java.lang.AbstractStringBuilder::append (62 bytes)   already compiled into a big method
       @ 21   Java.lang.StringBuilder::toString (17 bytes)   inline (hot)
         @ 13   Java.lang.String::<init> (62 bytes)   inline (hot)
           @ 1   Java.lang.Object::<init> (1 bytes)   inline (hot)
           @ 55   Java.util.Arrays::copyOfRange (63 bytes)   inline (hot)
             @ 54   Java.lang.Math::min (11 bytes)   (intrinsic)
             @ 57   Java.lang.System::arraycopy (0 bytes)   (intrinsic)
    

    Vous remarquerez peut-être que la version 7u55 est moins profonde, et il semble que rien ne soit appelé après les méthodes StringBuilder - c'est une bonne indication que les optimisations de chaîne sont en vigueur. En effet, si vous exécutez 7u55 avec -XX:-OptimizeStringConcat, les sous-appels réapparaîtront et les performances chuteront à 8u5.

  • OK, nous devons donc comprendre pourquoi 8u5 ne fait pas la même optimisation. Grep http://hg.openjdk.Java.net/jdk9/jdk9/hotspot pour "StringBuilder" pour savoir où VM gère l'optimisation StringConcat; cela permettra vous mettre dans src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp pour y découvrir les derniers changements. L'un des candidats serait:

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • Recherchez les fils de lecture sur les listes de diffusion OpenJDK (assez facile pour google pour un résumé des modifications): http://mail.openjdk.Java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • L'optimisation d'optimisation de concaténation "String" réduit le modèle [...] en une seule allocation d'une chaîne et forme directement le résultat. Toutes les déoptions possibles qui peuvent se produire dans le code optimisé redémarrent ce modèle depuis le début (à partir de l'allocation StringBuffer) Cela signifie que tout le motif doit être libre d'effets secondaires. "Eureka?

  • Écrivez la référence contrastée:

    @Fork(5)
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class IntStr {
        private int counter;
    
        @GenerateMicroBenchmark
        public String inlineSideEffect() {
            return new StringBuilder().append(counter++).toString();
        }
    
        @GenerateMicroBenchmark
        public String spliceSideEffect() {
            int cnt = counter++;
            return new StringBuilder().append(cnt).toString();
        }
    }
    
  • Mesurez-le sur JDK 7u55, en voyant les mêmes performances pour les effets secondaires intégrés/épissés:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       65.460        1.747    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       64.414        1.323    ns/op
    
  • Mesurez-le sur JDK 8u5, en voyant la dégradation des performances avec l'effet intégré:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       84.953        2.274    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       65.386        1.194    ns/op
    
  • Soumettez le rapport de bogue ( https://bugs.openjdk.Java.net/browse/JDK-8043677 ) pour discuter de ce comportement avec VM les gars. La justification de le correctif d'origine est solide, il est cependant intéressant si nous pouvons/devons récupérer cette optimisation dans certains cas triviaux comme ceux-ci.

  • ???

  • PROFIT.

Et oui, je devrais poster les résultats pour le benchmark qui déplace l'incrément de la chaîne StringBuilder, en le faisant avant toute la chaîne. Aussi, passé en temps moyen et ns/op. Voici JDK 7u55:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.805        1.093    ns/op
o.s.IntStr.stringBuilder0      avgt        25      128.284        6.797    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.524        3.116    ns/op
o.s.IntStr.stringBuilder2      avgt        25      254.384        9.204    ns/op
o.s.IntStr.stringFormat        avgt        25     2302.501      103.032    ns/op

Et c'est 8u5:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.032        3.295    ns/op
o.s.IntStr.stringBuilder0      avgt        25      127.796        1.158    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.585        1.137    ns/op
o.s.IntStr.stringBuilder2      avgt        25      250.980        2.773    ns/op
o.s.IntStr.stringFormat        avgt        25     2123.706       25.105    ns/op

stringFormat est en fait un peu plus rapide en 8u5, et tous les autres tests sont les mêmes. Cela solidifie l'hypothèse de la rupture d'effets secondaires dans les chaînes SB chez le principal coupable de la question d'origine.

94
Aleksey Shipilev

Je pense que cela a à voir avec le drapeau CompileThreshold qui contrôle quand le code d'octet est compilé en code machine par JIT.

Le JDK Oracle a un nombre par défaut de 10 000 comme document à http://www.Oracle.com/technetwork/Java/javase/tech/vmoptions-jsp-140102.html .

Où OpenJDK je n'ai pas pu trouver un dernier document sur ce drapeau; mais certains fils de discussion suggèrent un seuil beaucoup plus bas: http://mail.openjdk.Java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

Essayez également d'activer/désactiver les indicateurs Oracle JDK comme -XX:+UseCompressedStrings et -XX:+OptimizeStringConcat. Je ne sais pas si ces drapeaux sont activés par défaut sur OpenJDK. Quelqu'un pourrait-il suggérer.

Une expérience que vous pouvez faire consiste à exécuter le programme plusieurs fois, disons 30 000 boucles, à faire un System.gc (), puis à essayer de regarder les performances. Je pense qu'ils donneraient la même chose.

Et je suppose que votre paramètre GC est le même aussi. Sinon, vous allouez beaucoup d'objets et le GC pourrait bien être la majeure partie de votre temps d'exécution.

5
Alex Suo