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.
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 8La 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
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
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
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 buildtoolsIl 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
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.