Concaténation de chaînes ou bytecode de patch

J'ai récemment lu un article sur l'optimisation des performances du code Java - en particulier, la concaténation de chaînes. La question est restée en elle - pourquoi lors de l'utilisation de StringBuilder dans le code sous la coupe, le programme s'exécute plus lentement qu'avec une simple addition. Dans ce cas, + = pendant la compilation se transforme en appels StringBuilder.append ().

J'ai immédiatement eu envie de régler le problème.

// ~20 000 000    public String stringAppend() { String s = "foo"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; return s; } // ~7 000 000    public String stringAppendBuilder() { StringBuilder sb = new StringBuilder(); sb.append("foo"); sb.append(", bar"); sb.append(", bar"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); return sb.toString(); } 

Ensuite, tout mon raisonnement se résumait au fait qu'il s'agit d'une magie inexplicable à l'intérieur de la JVM, et j'ai abandonné en essayant de réaliser ce qui se passait. Cependant, lors de la prochaine discussion sur les différences entre les plates-formes dans la vitesse de travail avec les chaînes, mon ami yegorf1 et moi avons décidé de comprendre pourquoi et comment exactement cette magie se produit.

Oracle Java SE


upd: des tests ont été effectués sur Java 8
La solution évidente est de collecter le code source en bytecode, puis de regarder son contenu. Nous l'avons donc fait. Dans les commentaires, il a été suggéré que l'accélération est liée à l'optimisation - les lignes constantes devraient évidemment être collées ensemble au niveau de la compilation. Il s'est avéré que ce n'est pas le cas. Voici une partie du bytecode décompilé avec javap:

  public java.lang.String stringAppend(); Code: 0: ldc #2 // String foo 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: aload_1 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

Vous remarquerez peut-être qu'aucune optimisation n'a été effectuée. Étrange, non? Bon, voyons le bytecode de la deuxième fonction.

  public java.lang.String stringAppendBuilder(); Code: 0: new #3 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: pop 15: aload_1 16: ldc #6 // String , bar 18: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

Là encore, pas d'optimisations? De plus, regardons les instructions sur 8, 14 et 15 octets. Une chose étrange s'y produit - d'abord, une référence à un objet de la classe StringBuilder est chargée sur la pile, puis elle est jetée de la pile et rechargée. La solution la plus simple me vient à l'esprit:

  public java.lang.String stringAppendBuilder(); Code: 0: new #41 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

En jetant des instructions inutiles, nous obtenons un code qui fonctionne 1,5 fois plus vite que la version stringAppend, dans laquelle cette optimisation a déjà été effectuée. Ainsi, la faute de la «magie» est le compilateur de bytecode incomplet, qui ne peut pas effectuer des optimisations assez simples.

Android ART


upd: le code a été construit sous sdk 28 avec la version buildtools
Il s'est donc avéré que le problème est lié à l'implémentation du compilateur Java dans le bytecode de la pile JVM. Ici, nous nous sommes souvenus de l'existence de ART, qui fait partie du projet Android Open Source . Cette machine virtuelle, ou plutôt le compilateur de bytecode en code natif, a été écrite dans un procès d'Oracle, ce qui nous donne toutes les raisons de croire que les différences par rapport à l'implémentation d'Oracle sont importantes. De plus, en raison des spécificités des processeurs ARM, cette machine virtuelle est un registre, pas une pile.

Jetons un coup d'œil à Smali (l'une des représentations de bytecode sous ART):

 # virtual methods .method public stringAppend()Ljava/lang/String; .registers 4 .prologue .line 6 const-string/jumbo v0, "foo" .line 7 .local v0, "s":Ljava/lang/String; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 const-string/jumbo v2, ", bar" invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 //... .method public stringAppendBuilder()Ljava/lang/String; .registers 3 .prologue .line 13 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V .line 14 .local v0, "sb":Ljava/lang/StringBuilder; const-string/jumbo v1, "foo" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; .line 15 const-string/jumbo v1, ", bar" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; //... 

Dans cette variante stringAppendBuilder, il n'y a plus de problème avec la pile - la machine est un registre, et ils ne peuvent pas se produire en principe. Cependant, cela n'interfère pas avec l'existence de choses absolument magiques:

 move-result-object v1 

Cette chaîne dans stringAppend ne fait rien - le lien vers l'objet StringBuilder dont nous avons besoin est déjà dans le registre v1. Il serait logique de supposer que stringAppend fonctionnera plus lentement. Ceci est confirmé expérimentalement - le résultat est similaire au résultat de la version "corrigée" du programme pour la pile JVM: StringBuilder fonctionne presque une fois et demie plus vite.

Source: https://habr.com/ru/post/fr416479/


All Articles