76 votes

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

Tout en étudiant pour un peu de débat w.r.t. à l'aide de "" + n et Integer.toString(int) pour convertir un entier primitif à une chaîne que 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;
    }
}

J'ai couru avec la valeur par défaut JMH options avec les deux machines virtuelles Java qui existent sur ma machine Linux (up-to-date de Mageia 4 64 bits, Intel i7-3770 CPU, 32 go de RAM). La première JVM a été celui fourni 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 assez à 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'aide de l' StringBuilder classe est plus lent en raison de la charge supplémentaire de la création de l' StringBuilder objet et en ajoutant une chaîne vide. À l'aide de String.format(String, ...) est encore plus lente, par un ordre de grandeur.

La distribution fournis par le compilateur, 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 présentés ici ont été 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 est - StringBuilder.append(int) apparaissent de manière beaucoup plus rapide avec cette JVM? En regardant l' StringBuilder code source de la classe n'a rien révélé particulièrement intéressant - la méthode en question est presque identique à l' Integer#toString(int). Fait intéressant à noter, en y ajoutant le résultat de l' Integer.toString(int) ( stringBuilder2 microbenchmark) ne semble pas être plus rapide.

Est-ce la performance divergence d'un problème avec le test de harnais? Ou est-ce que mon OpenJDK JVM contiennent des optimisations qui pourraient affecter ce code particulier (anti)-motif?

EDIT:

Pour une plus straight-forward de comparaison, 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 de 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 c'est plus général de Java 7 vs Java 8 question. Peut-être Java 7 n'a plus agressive de la chaîne d'optimisations?

EDIT 2:

Pour être complet, ici sont liés à la chaîne VM options pour ces deux machines virtuelles:

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' UseStringCache option a été supprimée dans Java 8 avec pas de remplacement, donc je doute que cela fait toute la différence. Le reste des options semblent avoir les mêmes paramètres.

EDIT 3:

Un side-by-side de comparaison du code source de l' AbstractStringBuilder, StringBuilder et Integer des classes de l' src.zip le fichier de ne révèle rien de noteworty. En dehors de tout un tas de cosmétiques et de documentation des changements, Integer a maintenant un certain soutien pour les entiers non signés et StringBuilder a été légèrement remaniée pour partager du code avec StringBuffer. Aucun de ces changements ne semblent influer sur les chemins de code utilisée par StringBuilder#append(int),, bien que j'ai peut-être raté quelque chose.

Une comparaison du code assembleur 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() a été similaire pour les deux machines virtuelles, bien que Oracle JDK 8u5 semblait être plus agressif w.r.t. inline certains appels au sein de l' Integer#toString(int) code. Il y a une correspondance avec le code source Java, même pour quelqu'un avec un minimum d'expérience de montage.

Le code assembleur pour IntStr#stringBuilder0(), cependant, était radicalement différente. Le code généré par Oracle JDK 8u5 a été une fois de plus directement liées à la Java, le code source, j'ai pu 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 averti (comme le mien). L' new StringBuilder() appel a apparemment été supprimé, comme l'était la création de la matrice dans l' StringBuilder constructeur. De plus, le désassembleur plugin n'a pas été en mesure de fournir autant de références pour le code source, comme il l'a fait dans le JDK 8.

Je suppose que c'est soit le résultat d'une beaucoup plus agressives d'optimisation de passer dans OpenJDK 7, ou plus probablement à la suite de l'insertion d'écrit à la main au code de bas niveau pour certains StringBuilder des opérations. Je ne suis pas sûr pourquoi cette optimisation ne se fait pas dans mon JVM 8 mise en œuvre ou pourquoi les mêmes optimisations n'ont pas été mis en œuvre pour Integer#toString(int) de la JVM 7. Je suppose que quelqu'un de familier avec les parties connexes de la JRE code source aurait à répondre à ces questions...

96voto

Aleksey Shipilev Points 3758

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

Très bonne analyse de la question initiale et les mises à jour!

Pour être complet, voici quelques étapes manquantes:

  • Voir par l' -XX:+PrintInlining pour les deux 7u55 et 8u5. Dans 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 dans 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 7u55 version est moins profondes, et il semble que rien n'est appelée après l' StringBuilder méthodes -- ceci est une bonne indication de la chaîne d'optimisations sont en vigueur. En effet, si vous exécutez 7u55 avec -XX:-OptimizeStringConcat, le "subcalls" réapparaît, et le rendement tombe à 8u5 niveaux.

  • OK, donc nous avons besoin de comprendre pourquoi 8u5 ne fait pas de même de l'optimisation. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot pour "StringBuilder" pour comprendre où VM gère le StringConcat optimisation; cela vous mettra en src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp à la figure les dernières modifications. 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...
    
  • Regardez pour l'examen des threads sur OpenJDK listes de diffusion (assez facile pour google de révision résumé): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot "String concat optimisation optimisation s'effondre le modèle [...] en une allocation unique d'une chaîne et formant le résultat directement. Tout est possible deopts qui peuvent se produire dans le code optimisé pour le redémarrage de ce modèle depuis le début (à partir de la StringBuffer de répartition). Cela signifie que l'ensemble du motif doit m'sans effets secondaires." Eureka?

  • Écrire le contraste de référence:

    @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();
        }
    }
    
  • Mesure 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
    
  • Mesure 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 bug (https://bugs.openjdk.java.net/browse/JDK-8043677) afin de discuter de ce problème avec VM gars. La justification de l'origine correctif est solide comme un roc, il est intéressant cependant, si on peut/doit revenir cette optimisation dans certains cas triviaux comme celles-ci.

  • ???

  • Le PROFIT.

Et ouais, je devrais poster les résultats pour l'indice de référence qui se déplace à l'incrément de la StringBuilder chaîne, de le faire avant de l'ensemble de la chaîne. Aussi, passé à la moyenne du temps, 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 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 effectivement un peu plus rapide dans 8u5, et tous les autres tests sont les mêmes. Cela renforce l'hypothèse de l'effet secondaire de bris SB chaînes dans le grand coupable dans la question d'origine.

5voto

Alex Suo Points 1384

Je pense que cela a à voir avec l' CompileThreshold indicateur qui détermine le moment où le byte-code est compilé en code machine par ÉQUIPE.

L'Oracle JDK par défaut au nombre de 10 000 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 fils d'e-mail suggèrent un seuil bien plus faible: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

Aussi, essayez d'activer / désactiver l'Oracle JDK drapeaux comme -XX:+UseCompressedStrings et -XX:+OptimizeStringConcat. Je ne suis pas sûr si ces options sont 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 exécuter le programme par un grand nombre de fois, disons, de 30 000 boucles, faire un Système.gc (), puis essayez de regarder à la performance. Je crois qu'ils donneraient le même.

Et je suppose que votre GC réglage est le même aussi. Sinon, vous êtes attribution d'un lot d'objets et de la GC pourrait bien être la majeure partie de votre temps.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X