Neulich stolperte ich in meinem $dayjob im Java-Quelltext wieder einmal über ein logging-Statement, das die Meldung per String-Concatenation zusammensetzte. Bevor ich daraufhin wünschte, dass alle diese Statements auf VarArgs umgestellt werden, wollte ich mir eine solide Argumentationsgrundlage schaffen und stürzte dadurch in einen Kaninchenbau...
Es handelte sich bei dem Statement genasuer gesagt um ein solches hier:
LOGGER.trace("this "+some_obj+" is "+some_other_obj+" some concatenated text");
Zunächst einmal nochmals kurz zusammengefasst die (früher) gängige Lehrmeinung was Java betrifft: Man sollte statt des oben angegebenen Beispiels immer besser folgendes schreiben:
LOGGER.trace("this {} is a {} string made with varargs!",some_object,some_other_object);
Der Grund: die Methode trace im Beispiel stellt vielleicht fest, dass die Ausgabe gar nicht sein müsste. In der ersten Variante wird aber der String bereits zusammengesetzt und - falls some_obj gar keine Referenz ist, sondern ein Methodenaufruf - unter Umständen sehr teure Methodenaufrufe unnütz ausgeführt.
In der zweiten Methode wird der String nicht zusammengesetzt, sondern die Methode mit den Argumenten ausgeführt und erst nachdem die Methode festgestellt hat, dass es tatsächlich notwendig ist, den String zusammenzusetzen tut sie das dann auch. Falls - wie oben beschrieben - die Objekte keine zum Aufruf bekannte Referenzen, sondern inline-Methodenaufrufe sind, werden diese jedoch immer noch vor dem Aufruf der Methode trace ausgeführt - in diesen Fällen führt varargs also nicht zu Einsparungen. Nur was die Concatenation des Strings und die Kosten dafür angeht scheint die zweite Variante Vorteile zu haben.
Warum schrieb ich "scheint"? Nun - in frühen Versionen von Java war die Operation String-Concatenation mittels Additionsoperator wirklich furchtbar: sie erzeugte neue Instanzen, belastete den Garbage Collector und war ganz generell die Performance betreffend ein Alptraum.
Das war natürlich den Entwicklern nicht verborgen geblieben und der Java-Compiler begann, Optimierungen auszuführen, sobald er im Quelltext so etwas erkannte. Bei Java 8 zum Beispiel wurde implizit ein StringBuilder im Bytecode erzeugt, der dann die Aufgabe hatte, das Ergebnis zusammenzubauen - wie man im ByteCode für die folgende Beispielmethode sehen kann:
private void logStringConcat()
{
CLASS_LOGGER.trace("this "+java.lang.Integer.valueOf(rand.nextInt())+" is "+java.lang.Integer.valueOf(rand.nextInt())+" a "+java.lang.Integer.valueOf(rand.nextInt())+" concatenated "+java.lang.Integer.valueOf(rand.nextInt())+" string ");
}
private void logStringConcat();
Code:
0: getstatic #7 // Field CLASS_LOGGER:Lorg/slf4j/Logger;
3: new #8 // class java/lang/StringBuilder
6: dup
7: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
10: ldc #10 // String this
12: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #6 // Field rand:Ljava/util/Random;
19: invokevirtual #12 // Method java/util/Random.nextInt:()I
22: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
25: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
28: ldc #15 // String is
30: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: aload_0
34: getfield #6 // Field rand:Ljava/util/Random;
37: invokevirtual #12 // Method java/util/Random.nextInt:()I
40: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
43: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
46: ldc #16 // String a
48: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
51: aload_0
52: getfield #6 // Field rand:Ljava/util/Random;
55: invokevirtual #12 // Method java/util/Random.nextInt:()I
58: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
61: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
64: ldc #17 // String concatenated
66: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
69: aload_0
70: getfield #6 // Field rand:Ljava/util/Random;
73: invokevirtual #12 // Method java/util/Random.nextInt:()I
76: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
79: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
82: ldc #18 // String string
84: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
87: invokevirtual #19 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
90: invokeinterface #20, 2 // InterfaceMethod org/slf4j/Logger.trace:(Ljava/lang/String;)V
95: return
Vergleicht man das mit dem Bytecode der äquivalenten VarArgs-Variante, stellt man fest, dass der Aufruf der Methode trace ungefähr denselben Bedarf an ByteCode hat - das kommt daher, dass im Bytecode erst das Array konstruiert und bestückt werden muss, das intern den VarArgs-Aufruf ergibt:
private void logVarArgs()
{
CLASS_LOGGER.trace("this {} is {} a {} string {} made with varargs!",rand.nextInt(),rand.nextInt(),rand.nextInt(),rand.nextInt());
}
private void logVarArgs();
Code:
0: getstatic #7 // Field CLASS_LOGGER:Lorg/slf4j/Logger;
3: ldc #21 // String this {} is {} a {} string {} made with varargs!
5: iconst_4
6: anewarray #22 // class java/lang/Object
9: dup
10: iconst_0
11: aload_0
12: getfield #6 // Field rand:Ljava/util/Random;
15: invokevirtual #12 // Method java/util/Random.nextInt:()I
18: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: aastore
22: dup
23: iconst_1
24: aload_0
25: getfield #6 // Field rand:Ljava/util/Random;
28: invokevirtual #12 // Method java/util/Random.nextInt:()I
31: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
34: aastore
35: dup
36: iconst_2
37: aload_0
38: getfield #6 // Field rand:Ljava/util/Random;
41: invokevirtual #12 // Method java/util/Random.nextInt:()I
44: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
47: aastore
48: dup
49: iconst_3
50: aload_0
51: getfield #6 // Field rand:Ljava/util/Random;
54: invokevirtual #12 // Method java/util/Random.nextInt:()I
57: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
60: aastore
61: invokeinterface #23, 3 // InterfaceMethod org/slf4j/Logger.trace:(Ljava/lang/String;[Ljava/lang/Object;)V
66: return
Was den Gesamtaufwand angeht, muss man den irgendwann ja noch erfolgenden Aufwand für die eigentliche String-Concatenierung ebenfalls noch mit einrechnen. Das könnte das Gefühl aufkommen lassen, dass mit Java 8 die Methode der String-Concatenierung vorzuziehen sein könnte.
Wie ist es aber mit moderneren Versionen von Java? Irgendwann wurde invokeVirtual eingeführt. Damit wird dann plötzlich der Bytecode der ersten Variante mit String-Concatenation sehr viel kürzer wie in diesem Beispiel zu erkennen ist.
private void logStringConcat();
Code:
0: getstatic #28 // Field CLASS_LOGGER:Lorg/slf4j/Logger;
3: aload_0
4: getfield #22 // Field rand:Ljava/util/Random;
7: invokevirtual #32 // Method java/util/Random.nextInt:()I
10: invokestatic #36 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: aload_0
14: getfield #22 // Field rand:Ljava/util/Random;
17: invokevirtual #32 // Method java/util/Random.nextInt:()I
20: invokestatic #36 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
23: aload_0
24: getfield #22 // Field rand:Ljava/util/Random;
27: invokevirtual #32 // Method java/util/Random.nextInt:()I
30: invokestatic #36 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
33: aload_0
34: getfield #22 // Field rand:Ljava/util/Random;
37: invokevirtual #32 // Method java/util/Random.nextInt:()I
40: invokestatic #36 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
43: invokedynamic #42, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/String;
48: invokeinterface #46, 2 // InterfaceMethod org/slf4j/Logger.trace:(Ljava/lang/String;)V
53: return
Folgerichtig fragt man sich, wie das dann noch funktionieren kann - die Antwort ist zu finden, wenn man nach der Methode, die hier aufgerufen wird recherchiert: Neuere Java-versionen erzeugen beim Startup dafür passenden ByteCode, der die String-Concatenation ausführt und die entsprechende Callsite wird anschließend dort eingetragen, wo eigentlich eine String-Concatenation benötigt wird. Spätestens dann ist es nicht mehr möglich, am Code zu entscheiden welches von beiden die bessere weil ressourcenschonendere Variante ist. Klar ist nur noch, dass Variante eins die Startupzeit einer Anwendung verlängert, da für jede Stringconcatenation eine passende Bytecode-Implementierung erstellt werden muss bevor die Anwendung startet. Es kann sogar so sein, dass dieselbe Anwendung abhängig von der benutzten Runtime und Architektur mal in der einen und mal in der anderen Version performanter ist und man bei der eigentlichen Programmierung gar keinen Gedanken mehr daran verschwenden muss, weil die Laufzeitumgehbung so einen großen Einfluss hat und man diese während der Entwicklung nicht beeinflussen kann.
Multi-User-WebDAV, Docker, GitHub
17.11.2019
Nachdem ich mich in letzter Zeit verstärkt mit Docker und dem zugehörigen Ökosystem beschäftige, habe ich begonnen, verschiedenste Dienste in Containern zu testen um zu sehen, ob in manchen Fällen LXC oder KVM nicht doch die bessere Wahl wäre...
Weiterlesen...Android Basteln C und C++ Chaos Datenbanken Docker dWb+ ESP Wifi Garten Geo Go GUI Gui Hardware Java Jupyter Komponenten Links Linux Markdown Markup Music Numerik OpenSource PKI-X.509-CA Python QBrowser Rants Raspi Revisited Security Software-Test sQLshell TeleGrafana Verschiedenes Video Virtualisierung Windows Upcoming...
In eigener Sache...
Weiterlesen...Ich habe eine neue Java Swing Komponente erstellt: Es handelt sich um einen Wrapper für von JToolBar abgeleitete Klassen, die die Werkzeugleiste minimieren und sie nur dann einblenden, wenn der Mauszeiger über ihnen schwebt.
Weiterlesen...Ich habe bereits in einem früheren Artikel über meine ersten Erfolge berichtet, der sQLshell auf Basis des bestehenden Codes aus dem Projekt EBMap4D eine bessere Integration für Geo-Daten zu spendieren und entsprechende Abfragen, bzw. deren Ergebnisse auf einer Kartenansicht zu visualisieren.
Weiterlesen...Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.
Wer daran teilhaben und eventuell sogar davon profitieren möchte, muss damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.
Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...
PS: Meine öffentlichen Codeberg-Repositories findet man hier.