Pourquoi StringBuilder#append (int) est-il plus rapide en Java 7 qu'en Java 8?

While investigating for a little debate W. R. T. en utilisant "" + n et Integer.toString(int) pour convertir un entier primitif en chaîne, j'ai écrit ce 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 lancé avec les options JMH par défaut avec les deux VM Java qui existent sur ma machine Linux (Mageia à jour 64 bits, Intel I7-3770 CPU, 32 Go RAM). La première JVM a été 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 JVM j'ai eu à 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

i. e. l'utilisation de la classe StringBuilder est plus lente en raison des frais généraux supplémentaires liés à la création de l'objet StringBuilder et à l'ajout d'une chaîne de caractères vide. L'utilisation de String.format(String, ...) est encore plus lente, d'un ordre de grandeur.

le compilateur fourni par 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éressant :

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 tellement plus vite avec cette JVM? 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) . Chose intéressante, ajouter le résultat de Integer.toString(int) (le stringBuilder2 microbenchmark) ne semble pas être plus rapide.

cet écart de rendement pose-t-il un problème avec le harnais d'essai? Ou est-ce que mon OpenJDK JVM contient des optimisations qui affecteraient ce code (anti) - pattern particulier?

EDIT:

pour une comparaison plus directe, 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 de Java 7 par rapport à Java 8. Peut-être que Java 7 avait des optimisations de chaînes plus agressives?

EDIT 2 :

pour plus d'exhaustivité, voici les options VM liées aux chaînes de caractères pour ces deux JVM:

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 aucun remplacement, donc I le doute qui fait toute la différence. Les autres options semblent avoir les mêmes paramètres.

EDIT 3:

une comparaison side-by-side du code source des classes AbstractStringBuilder , StringBuilder et Integer du fichier src.zip ne révèle rien de noteworty. En dehors d'un grand nombre de changements cosmétiques et de la documentation, Integer a maintenant un certain soutien pour les entiers non signés et StringBuilder a a été légèrement remanié pour partager plus de code avec StringBuffer . Aucun de ces changements ne semble affecter les chemins de code utilisés par StringBuilder#append(int) , bien que j'ai pu manquer quelque chose.

une comparaison du code d'assemblage 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 JVM, bien que Oracle JDK 8u5 semble être plus agressif W. R. T. en ligne certains appels à l'intérieur de la Integer#toString(int) du code. Il y avait une correspondance claire avec le code source Java, même pour quelqu'un avec une expérience minimale d'assemblage.

le code d'assemblage pour IntStr#stringBuilder0() était radicalement différent. Le code généré par Oracle JDK 8u5 était à nouveau directement lié au code source Java - je pouvais facilement reconnaître la même mise en page. Au contraire, le code généré par OpenJDK 7 était presque méconnaissable à l'œil non formé (comme le mien). Le new StringBuilder() l'appel a été apparemment supprimé, tout comme la création du tableau dans le constructeur StringBuilder . De plus, le plugin de démontage n'a pas pu fournir autant de références au code source que dans JDK 8.

je suppose que c'est soit le résultat d'un passage d'optimisation beaucoup plus agressif dans OpenJDK 7, soit plus probablement le résultat de l'insertion de code de bas niveau écrit à la main pour certaines opérations StringBuilder . Je ne sais pas pourquoi cette optimisation ne se produire dans ma mise en œuvre JVM 8 ou pourquoi les mêmes optimisations n'ont pas été mises en œuvre pour Integer#toString(int) dans JVM 7. Je suppose que quelqu'un qui connaît les parties du code source de JRE devrait répondre à ces questions...

76
demandé sur Community 2014-05-20 14:13:51

2 réponses

TL;DR: effets Secondaires append apparemment pause StringConcat optimisations.

très bonne analyse dans la question originale et 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 pourriez remarquer que la version 7u55 est moins profonde, et il semble que rien n'est appelé après les méthodes StringBuilder -- c'est une bonne indication que les optimisations des chaînes sont en vigueur. En effet , si vous exécutez 7u55 avec -XX:-OptimizeStringConcat , les sous-appels réapparaîtront, et la performance descendra à 8u5 niveaux.

  • OK, donc on doit trouver 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 vous mènera dans src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp pour comprendre les derniers changements. L'un des candidats:

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • chercher passer en revue les threads sur les listes de diffusion OpenJDK (assez facile à google pour le résumé de changeset): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot " String optimization concat optimization faillis the pattern [...] en une seule attribution d'une chaîne et formant le résultat directement. Tous les désopulations possibles qui peuvent se produire dans le code optimisé redémarrez Ce modèle dès le début (à partir de L'allocation StringBuffer). cela signifie que l'ensemble du schéma doit être exempt d'effets secondaires. " Eureka?

  • écrire le benchmark contrastant:

    @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();
        }
    }
    
  • Mesurer sur JDK 7u55, de voir les mêmes performances pour inline/épissé effets secondaires:

    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
    
  • Mesurer sur JDK 8u5, en voyant la dégradation des performances avec les inline effet:

    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
    
  • soumettre le rapport de bogue ( https://bugs.openjdk.java.net/browse/JDK-8043677 ) pour discuter de ce comportement avec les VM guys. La raison d'être de la solution originale est solide comme la roche, il est intéressant cependant si nous pouvons / devrions récupérer cette optimisation dans certains cas triviaux comme ils.

  • ???

  • PROFIT.

et oui, je devrais poster les résultats pour le benchmark qui déplace l'incrément de la chaîne StringBuilder , le faisant avant toute la chaîne. Aussi, commuté à temps moyen, et ns/op. C'est 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 ici 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. Ceci consolide l'hypothèse de la rupture des effets secondaires dans les chaînes SB dans le principal coupable de la question originale.

94
répondu Aleksey Shipilev 2014-05-21 20:37:23

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

L'Oracle JDK 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 ne pouvais pas trouver un dernier document sur ce drapeau; mais certains threads de courrier suggèrent un seuil beaucoup plus bas: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

aussi, essayez d'allumer / éteindre les drapeaux Oracle JDK comme -XX:+UseCompressedStrings et -XX:+OptimizeStringConcat . Je ne suis pas sûr que ces options soient activées par défaut sur OpenJDK. Quelqu'un pourrait s'il vous plaît suggérer.

une expérience que vous pouvez faire, est d'abord d'exécuter le programme par un grand nombre de fois, disons, 30.000 boucles, faire un système.gc () et ensuite essayer de regarder la performance. Je je crois qu'ils donneraient la même chose.

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

5
répondu Alex Suo 2014-05-20 10:36:16