Comment la concaténation de chaînes est-elle implémentée dans Java 9?

111

Comme écrit dans JEP 280: Indify String Concatenation :

Modifiez la Stringséquence de bytecode -concatenation statique générée par javacpour utiliser les invokedynamicappels aux fonctions de la bibliothèque JDK. Cela permettra des optimisations futures de la Stringconcaténation sans nécessiter de modifications supplémentaires du bytecode émis par javac.

Ici, je veux comprendre ce qu'est l'utilisation des invokedynamicappels et en quoi la concaténation de bytecode est différente de invokedynamic?

Mohit Tyagi
la source
11
J'ai écrit à ce sujet il y a quelque temps - si cela aide, je vais le condenser en une réponse.
Nicolai
10
Jetez également un œil à cette vidéo qui explique bien l'intérêt du nouveau mécanisme de concaténation de chaînes: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov
3
@ZhekaKozlov J'aurais aimé pouvoir voter deux fois pour votre commentaire, les liens qui proviennent de personnes qui mettent en œuvre tout cela sont les meilleurs.
Eugene
2
@Nicolai: Ce serait génial, et ce serait une meilleure réponse que n'importe quelle autre ici (y compris la mienne). Toutes les parties de ma réponse que vous souhaitez intégrer lorsque vous le faites, n'hésitez pas - si vous incluez (essentiellement) le tout dans le cadre de la réponse plus large, je supprimerai simplement la mienne. Alternativement, si vous voulez simplement ajouter à ma réponse car elle est assez visible, j'en ai fait un wiki communautaire.
TJ Crowder

Réponses:

95

L '«ancienne» méthode produit un tas d' StringBuilderopérations orientées. Considérez ce programme:

public class Example {
    public static void main(String[] args)
    {
        String result = args[0] + "-" + args[1] + "-" + args[2];
        System.out.println(result);
    }
}

Si nous compilons cela avec JDK 8 ou une version antérieure et que nous utilisons ensuite javap -c Examplepour voir le bytecode, nous voyons quelque chose comme ceci:

Exemple de classe publique {
  Exemple public ();
    Code:
       0: aload_0
       1: invokespecial # 1 // Méthode java / lang / Object. "<init>" :() V
       4: retour

  public static void main (java.lang.String []);
    Code:
       0: nouveau # 2 // classe java / lang / StringBuilder
       3: dup
       4: invokespecial # 3 // Méthode java / lang / StringBuilder. "<init>" :() V
       7: aload_0
       8: iconst_0
       9: une charge
      10: invokevirtual # 4 // Méthode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      13: ldc # 5 // Chaîne -
      15: invokevirtual # 4 // Méthode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      18: aload_0
      19: iconst_1
      20: aaload
      21: invokevirtual # 4 // Méthode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      24: ldc # 5 // Chaîne -
      26: invokevirtual # 4 // Méthode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      29: aload_0
      30: iconst_2
      31: aaload
      32: invokevirtual # 4 // Méthode java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      35: invokevirtual # 6 // Méthode java / lang / StringBuilder.toString :() Ljava / lang / String;
      38: astore_1
      39: getstatic # 7 // Champ java / lang / System.out: Ljava / io / PrintStream;
      42: aload_1
      43: invokevirtual # 8 // Méthode java / io / PrintStream.println: (Ljava / lang / String;) V
      46: retour
}

Comme vous pouvez le voir, il crée un StringBuilderet utilise append. Ceci est réputé assez inefficace car la capacité par défaut du tampon intégré StringBuildern'est que de 16 caractères, et il n'y a aucun moyen pour le compilateur de savoir d'allouer plus à l'avance, donc il finit par devoir réallouer. C'est aussi un tas d'appels de méthode. (Notez que la JVM peut parfois détecter et réécrire ces modèles d'appels pour les rendre plus efficaces, cependant.)

Regardons ce que génère Java 9:

Exemple de classe publique {
  Exemple public ();
    Code:
       0: aload_0
       1: invokespecial # 1 // Méthode java / lang / Object. "<init>" :() V
       4: retour

  public static void main (java.lang.String []);
    Code:
       0: aload_0
       1: iconst_0
       2: une charge
       3: aload_0
       4: iconst_1
       5: une charge
       6: aload_0
       7: iconst_2
       8: une charge
       9: invokedynamic # 2, 0 // InvokeDynamic # 0: makeConcatWithConstants: (Ljava / lang / String; Ljava / lang / String; Ljava / lang / String;) Ljava / lang / String;
      14: astore_1
      15: getstatic # 3 // Champ java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // Méthode java / io / PrintStream.println: (Ljava / lang / String;) V
      22: retour
}

Oh mon mais c'est plus court. :-) Il fait un seul appel à makeConcatWithConstantsfrom StringConcatFactory, qui dit ceci dans son Javadoc:

Méthodes pour faciliter la création de méthodes de concaténation String, qui peuvent être utilisées pour concaténer efficacement un nombre connu d'arguments de types connus, éventuellement après adaptation de type et évaluation partielle des arguments. Ces méthodes sont généralement utilisées comme méthodes d'amorçage pour invokedynamicles sites d'appel, pour prendre en charge la fonction de concaténation de chaînes du langage de programmation Java.

T.J. Crowder
la source
41
Cela me rappelle une réponse que j'ai écrite il y a presque 6 ans jour pour jour: stackoverflow.com/a/7586780/330057 - Quelqu'un a demandé s'il devait créer un StringBuilder ou simplement utiliser un ancien +=dans sa boucle for. Je leur ai dit que cela dépendait, mais n'oublions pas qu'ils pourraient trouver un meilleur moyen d'enchaîner le concat un jour plus tard. La ligne clé est vraiment l'avant-dernière ligne:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa
3
@corsiKa: LOL! Mais wow, il a fallu beaucoup de temps pour y arriver (je ne veux pas dire six ans, je veux dire 22 ou plus ... :-))
TJ Crowder
1
@supercat: Si je comprends bien, il y a plusieurs raisons, notamment que la création d'un tableau varargs à passer à une méthode sur un chemin critique pour les performances n'est pas idéale. De plus, l'utilisation invokedynamicpermet de choisir différentes stratégies de concaténation au moment de l'exécution et de les relier lors du premier appel, sans la surcharge d'un appel de méthode et d'une table de répartition à chaque appel; plus dans l' article de nicolai ici et dans le JEP .
TJ Crowder
1
@supercat: Et puis il y a le fait qu'il ne jouerait pas bien avec les non-Strings, car ils devraient être pré-convertis en String plutôt que d'être convertis en résultat final; plus d'inefficacité. Pourrait le faire Object, mais alors vous devriez boxer tous les primitifs ... (Ce que Nicolai couvre dans son excellent article, btw.)
TJ Crowder
2
@supercat Je faisais référence à la String.concat(String)méthode déjà existante dont l'implémentation crée le tableau de la chaîne résultante sur place. L'avantage devient sans objet lorsque nous devons invoquer toString()sur des objets arbitraires. De même, lors de l'appel d'une méthode acceptant un tableau, l'appelant doit créer et remplir le tableau, ce qui réduit l'avantage global. Mais maintenant, ce n'est pas pertinent, car la nouvelle solution est essentiellement ce que vous envisagiez, sauf qu'elle n'a pas de surcharge de boxe, ne nécessite pas de création de tableau et que le backend peut générer des gestionnaires optimisés pour des scénarios particuliers.
Holger
20

Avant d'entrer dans les détails de l' invokedynamicimplémentation utilisée pour l'optimisation de la concaténation de chaînes, à mon avis, il faut se familiariser avec ce qui est invokedynamic et comment l'utiliser?

L' invokedynamic instruction simplifie et améliore potentiellement les implémentations de compilateurs et de systèmes d'exécution pour les langages dynamiques sur la JVM . Pour ce faire, il permet à l'implémenteur du langage de définir un comportement de liaison personnalisé avec l' invokedynamicinstruction qui implique les étapes suivantes.


J'essaierais probablement de vous expliquer les changements apportés pour l'implémentation de l'optimisation de la concaténation de chaînes.

  • Définition de la méthode Bootstrap : - Avec Java9, les méthodes bootstrap pour invokedynamicles sites d'appel, pour prendre en charge la concaténation de chaînes principalement makeConcatet makeConcatWithConstantsont été introduites avec l' StringConcatFactoryimplémentation.

    L'utilisation de invokedynamic offre une alternative pour sélectionner une stratégie de traduction jusqu'à l'exécution. La stratégie de traduction utilisée dans StringConcatFactoryest similaire à LambdaMetafactorycelle introduite dans la version java précédente. En outre, l'un des objectifs du PEC mentionné dans la question est d'étendre ces stratégies davantage.

  • Spécification des entrées du pool de constantes : - Ce sont les arguments statiques supplémentaires à l' invokedynamicinstruction autres que (1) MethodHandles.Lookupobjet qui est une fabrique pour créer des descripteurs de méthode dans le contexte de l' invokedynamicinstruction, (2) un Stringobjet, le nom de la méthode mentionné dans l'appel dynamique site et (3) l' MethodTypeobjet, la signature de type résolue du site d'appel dynamique.

    Il y a déjà des liens lors de la liaison du code. Au moment de l'exécution, la méthode d'amorçage s'exécute et les liens dans le code réel effectuant la concaténation. Il réécrit l' invokedynamicappel avec un invokestaticappel approprié . Cela charge la chaîne constante à partir du pool de constantes, les arguments statiques de la méthode bootstrap sont exploités pour transmettre ces constantes et d'autres directement à l'appel de la méthode bootstrap.

  • Utilisation de l'instruction invokedynamic : - Ceci offre les facilités pour une liaison paresseuse, en fournissant les moyens d'amorcer la cible d'appel une fois, pendant l'appel initial. L'idée concrète d'optimisation ici est de remplacer toute la StringBuilder.appenddanse par un simple invokedynamicappel à java.lang.invoke.StringConcatFactory, qui acceptera les valeurs dans le besoin de concaténation.

La proposition Indify String Concatenation indique avec un exemple l'analyse comparative de l'application avec Java9 où une méthode similaire à celle partagée par @TJ Crowder est compilée et la différence du bytecode est assez visible entre les différentes implémentations.

Naman
la source
17

Je vais ajouter un peu de détails ici. La principale partie à obtenir est que la façon dont la concaténation de chaînes est effectuée est une décision d'exécution, et non plus une décision de compilation . Ainsi, cela peut changer, ce qui signifie que vous avez compilé votre code une fois contre java-9 et qu'il peut changer l'implémentation sous-jacente comme bon lui semble, sans avoir besoin de recompiler.

Et le deuxième point est qu'en ce moment il y a 6 possible strategies for concatenation of String:

 private enum Strategy {
    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder}.
     */
    BC_SB,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but trying to estimate the required storage.
     */
    BC_SB_SIZED,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but computing the required storage exactly.
     */
    BC_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also tries to estimate the required storage.
     */
    MH_SB_SIZED,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also estimate the required storage exactly.
     */
    MH_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that constructs its own byte[] array from
     * the arguments. It computes the required storage exactly.
     */
    MH_INLINE_SIZED_EXACT
}

Vous pouvez choisir l' un d'eux via un paramètre: -Djava.lang.invoke.stringConcat. Notez que StringBuilderc'est toujours une option.

Eugène
la source