String-Verkettung oder Patch-Bytecode

Ich habe kürzlich einen Artikel über die Optimierung der Leistung von Java-Code gelesen - insbesondere über die Verkettung von Zeichenfolgen. Die Frage blieb dabei: Warum läuft das Programm bei Verwendung von StringBuilder im Code unter dem Schnitt langsamer als bei einfacher Hinzufügung? In diesem Fall wird + = während der Kompilierung zu StringBuilder.append () -Aufrufen.

Ich hatte sofort den Wunsch, das Problem zu lösen.

// ~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(); } 

Dann kamen alle meine Überlegungen auf die Tatsache an, dass dies unerklärliche Magie innerhalb der JVM ist, und ich gab es auf, zu versuchen, zu erkennen, was geschah. Während der nächsten Diskussion über die Unterschiede zwischen Plattformen in der Geschwindigkeit der Arbeit mit Strings entschied sich mein Freund yegorf1 jedoch herauszufinden, warum und wie genau diese Magie geschieht.

Oracle Java SE


upd: Tests wurden unter Java 8 durchgeführt
Die naheliegende Lösung besteht darin, den Quellcode im Bytecode zu sammeln und dann seinen Inhalt zu überprüfen. Also haben wir es getan. In den Kommentaren gab es Vorschläge, dass Beschleunigung mit Optimierung zusammenhängt - konstante Linien sollten offensichtlich auf Kompilierungsebene zusammengeklebt werden. Es stellte sich heraus, dass dies nicht so ist. Hier ist ein Teil des mit javap dekompilierten Bytecodes:

  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; 

Möglicherweise stellen Sie fest, dass keine Optimierungen vorgenommen wurden. Seltsam, nicht wahr? Okay, sehen wir uns den Bytecode der zweiten Funktion an.

  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; 

Auch hier keine Optimierungen? Schauen wir uns außerdem die Anweisungen zu 8, 14 und 15 Bytes an. Dort passiert etwas Seltsames: Zuerst wird ein Verweis auf ein Objekt der StringBuilder-Klasse auf den Stapel geladen, dann vom Stapel geworfen und neu geladen. Die einfachste Lösung fällt mir ein:

  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; 

Wenn wir unnötige Anweisungen wegwerfen, erhalten wir einen Code, der 1,5-mal schneller funktioniert als die stringAppend-Version, in der diese Optimierung bereits durchgeführt wurde. Der Fehler der „Magie“ ist also der unvollständige Bytecode-Compiler, der keine recht einfachen Optimierungen durchführen kann.

Android ART


upd: der code wurde unter sdk 28 mit den release buildtools erstellt
Es stellte sich also heraus, dass das Problem mit der Implementierung des Java-Compilers im Bytecode für die Stack-JVM zusammenhängt. Hier erinnerten wir uns an die Existenz von ART, die Teil des Android Open Source-Projekts ist . Diese virtuelle Maschine, oder besser gesagt der Bytecode-Compiler, in nativen Code, wurde unter den Bedingungen einer Klage von Oracle geschrieben, was uns allen Grund zu der Annahme gibt, dass die Unterschiede zur Oracle-Implementierung erheblich sind. Aufgrund der Besonderheiten von ARM-Prozessoren ist diese virtuelle Maschine außerdem ein Register und kein Stapel.

Werfen wir einen Blick auf Smali (eine der Bytecode-Darstellungen unter 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; //... 

In dieser Variante stringAppendBuilder gibt es keine Probleme mehr mit dem Stack - die Maschine ist ein Register und sie können im Prinzip nicht auftreten. Dies beeinträchtigt jedoch nicht die Existenz absolut magischer Dinge:

 move-result-object v1 

Diese Zeichenfolge in stringAppend bewirkt nichts - der Link zum benötigten StringBuilder-Objekt befindet sich bereits im v1-Register. Es wäre logisch anzunehmen, dass stringAppend langsamer arbeitet. Dies wird experimentell bestätigt - das Ergebnis ähnelt dem Ergebnis der "gepatchten" Version des Programms für die Stapel-JVM: StringBuilder arbeitet fast eineinhalb Mal schneller.

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


All Articles