Ich habe mich wieder einmal hingesetzt und die Art und Weise, wie Java Bytecode erstellt und verarbeitet, untersucht
Die Untersuchung soll sich zunächst einmal auf die Größe des Bytecode konzentrieren. Danach wird diskutiert, wie sich die einzelnen Varianten in Hinblick auf die Abarbeitungsgeschwindigkeit verhalten.
Ich möchte hier gleich betonen: es geht um Bytecode - was der Just-in-time-Compiler dann daraus macht und wie der Bytecode auf tatsächliche Maschineninstruktionen abgebildet wird, wird nicht Teil dieser Analyse sein!
Nun kann man sich fragen: "Schleifen gibts denn da solche Unterschiede?" Ich habe mich von einem Artikel im schönen Internet inspirieren lassen - der zeigte für low-Level-Sprachen, dass es durchaus überlegenswerte Abwäghungen gibt, die einen messbaren Einföuss auf beide genannten Messgrößen haben können. Daher wollte ich das auch für Java Bytecode untersuchen.
Außerdem sind da noch die verknöcherten Ansichten eines alten weißen Mannes, der vor vielen Jahrzehnten einmal gelernt hat, dass sich die Performanz von Postfix- und Prefix-Operatoren unterscheidet und der seither seine for Schleifen wie folgt schreibt:
for(int i=0;i<5;++i)
statt
for(int i=0;i<5;i++)
Das sollte gleich das erste Ziel der Untersuchung sein - also die Unterschiede zwischen diesen beiden Varianten:
public void call1()
{
long akku=0;
for(int i=0;i<getIterationCount();++i)
{
akku+=System.currentTimeMillis();
}
}
public void call2()
{
long akku=0;
for(int i=0;i<getIterationCount();i++)
{
akku+=System.currentTimeMillis();
}
}
Der Bytecode dafür sieht wie folgt aus:
public void call1();
Code:
0: lconst_0
1: lstore_1
2: iconst_0
3: istore_3
4: iload_3
5: aload_0
6: invokevirtual #119 // Method getIterationCount:()I
9: if_icmpge 24
12: lload_1
13: invokestatic #122 // Method java/lang/System.currentTimeMillis:()J
16: ladd
17: lstore_1
18: iinc 3, 1
21: goto 4
24: return
public void call2();
Code:
0: lconst_0
1: lstore_1
2: iconst_0
3: istore_3
4: iload_3
5: aload_0
6: invokevirtual #119 // Method getIterationCount:()I
9: if_icmpge 24
12: lload_1
13: invokestatic #122 // Method java/lang/System.currentTimeMillis:()J
16: ladd
17: lstore_1
18: iinc 3, 1
21: goto 4
24: return
Wie man sieht - überhaupt kein Unterschied! - Das liegt daran, dass die Variable i in der Schleife nicht verwendet wird, wie man weiter unten in den Beispielen mit den repeat Schleifen und dem zugehörigen Quellcode erkennen kann (und wir merken uns mal - die Länge des Bytecodes für die eigentliche Schleife beträgt hier 17).
Aber zunächst zu einem weiteren Test, der sich nahe an den verlinkten Artikel schmiegt: Wir haben oben mit einer beliebigen Zahl als Abbruchkriterium verglichen - ändert sich etwas an der Gestalt und/oder Länge des Bytecode wenn das Abbruchkriterium immer 0 oder -1 ist?
public void call3()
{
long akku=0;
for(int i=getIterationCount()-1;i!=0;--i)
{
akku+=System.currentTimeMillis();
}
}
public void call4()
{
long akku=0;
for(int i=getIterationCount()-1;i>-1;--i)
{
akku+=System.currentTimeMillis();
}
}
Der zugehörige Bytecode:
public void call3();
Code:
0: lconst_0
1: lstore_1
2: aload_0
3: invokevirtual #119 // Method getIterationCount:()I
6: iconst_1
7: isub
8: istore_3
9: iload_3
10: ifeq 25
13: lload_1
14: invokestatic #122 // Method java/lang/System.currentTimeMillis:()J
17: ladd
18: lstore_1
19: iinc 3, -1
22: goto 9
25: return
public void call4();
Code:
0: lconst_0
1: lstore_1
2: aload_0
3: invokevirtual #119 // Method getIterationCount:()I
6: iconst_1
7: isub
8: istore_3
9: iload_3
10: iconst_m1
11: if_icmple 26
14: lload_1
15: invokestatic #122 // Method java/lang/System.currentTimeMillis:()J
18: ladd
19: lstore_1
20: iinc 3, -1
23: goto 9
26: return
Und ja - es ist ein Unterschied zu entdecken: Die Variante, die auf 0 testet ist zunächst einmal deutlich kürzer: in Bytecode gemessen ist die Länge der eigentlichen Schleife nur noch 13 bzw. 14 bei der Variante mit Test auf -1. Der Unterschied zum vorangegangenen Test ist hier hauptsächlich auf die Tatsache zurückzuführen, dass die Methode zur Bestimmung der Anzahl der Iterationen nicht mehr in jedem Schleifendurchlauf aufgerufen wird, sondern nur noch zu Beginn.
Bisher haben wir uns auf for Schleifen konzentriert. Sieht es eventuell mit anderen syntaktischen Konstrukten anders aus? Was ist mit while Schleifen?
public void call6()
{
int a=getIterationCount();
long akku=0;
while(a--!=0)
{
akku+=System.currentTimeMillis();
}
}
public void call6();
Code:
0: aload_0
1: invokevirtual #119 // Method getIterationCount:()I
4: istore_1
5: lconst_0
6: lstore_2
7: iload_1
8: iinc 1, -1
11: ifeq 23
14: lload_2
15: invokestatic #122 // Method java/lang/System.currentTimeMillis:()J
18: ladd
19: lstore_2
20: goto 7
23: return
Die Länge beträgt hier analog zum vorhergehenden Beipiel wieder 13 (Vergleich und Abbruchbedingung wieder 0).
Nun zur Frage, ob sich mittels repeat Schleifen vielleicht noch etwas machen lässt?
public void call7()
{
int a=getIterationCount()-1;
long akku=0;
if(a>0)
{
do
{
akku+=System.currentTimeMillis();
}
while(--a>=0);
}
}
public void call8()
{
int a=getIterationCount()-1;
if(a>0)
{
long akku=0;
do
{
akku+=System.currentTimeMillis();
}
while(a--!=0);
}
}
Hier wurde wieder auch der Vergleich zwischen Prefix- und Postfix-Operator gezogen und es existiert ein Unterschied:
public void call7();
Code:
0: aload_0
1: invokevirtual #119 // Method getIterationCount:()I
4: iconst_1
5: isub
6: istore_1
7: lconst_0
8: lstore_2
9: iload_1
10: ifle 26
13: lload_2
14: invokestatic #122 // Method java/lang/System.currentTimeMillis:()J
17: ladd
18: lstore_2
19: iinc 1, -1
22: iload_1
23: ifge 13
26: return
public void call8();
Code:
0: aload_0
1: invokevirtual #119 // Method getIterationCount:()I
4: iconst_1
5: isub
6: istore_1
7: iload_1
8: ifle 26
11: lconst_0
12: lstore_2
13: lload_2
14: invokestatic #122 // Method java/lang/System.currentTimeMillis:()J
17: ladd
18: lstore_2
19: iload_1
20: iinc 1, -1
23: ifne 13
26: return
Die Länge der eigentlichen Schleife beträgt nur noch lediglich 10! (und man sieht hier auch schön den Unterschied zwischen Prefix- und Postfix-Operator, erkennt aber auch, dass das heutzutage und in Hochsprachen herzlich egal ist!).
Nun stellt sich natürlich die Frage: wenn wir einen solch drastischen Unterschied in der Größe sehen - wirkt sich das irgendwie auf die Laufzeit aus? Ich habe dazu 4000 Mal jede der Schleifen laufen lassen und dabei jeweils 500000 Mal iteriert - vorher habe ich durch 5-maligen Durchlauf durch diese 500000 Iterationen dafür gesorgt, dass sich der JIT eingeschwungen hat und das Ergebnis nur noch die reine Ausführungszeit des eigentlichen Maschinencodes misst. Die Ergebnisse wurden über die 40 Läufe gemittelt und es stellt sich heraus, dass die naive Version, in der jedesmal die Funktion zur Bestimmung der Abbruchbedingung aufgerufen wird für die 500000 Durchläufe (auf meinem Testsystem: Intel) genauso viel Zeit benötigt, wie die auf dem Papier kürzeste call8. Man sieht wieder einmal - premature optimization is the root of all evil!
Aus Neugierde habe ich diesen Test auch noch einmal auf einem ARM (Rock64 - Rockchip RK3328 quad-core ARM Cortex A53 64-Bit Processor) durchgeführt, wo sich die vom JIT-Compiler erzeugten Instruktionen ja prinzipiell deutlich unterscheiden sollten und vom Ergebnis her ein wenig abwichen: Die Variante call1 schnitt deutlich besser ab als alle anderen - allerdings möchte ich nochmals zu bedenken geben, dass die Art meines Tests und die geringe Stichprobenanzahl nicht das Umstellen aller Schleifen in einer Codebasis rechtfertigt!
Links - Verschiedenes II
02.12.2020
Heute wieder eine Sammlung wild durch den Kräutergarten - diesmal hauptächlich zum Thema Kubernetes.
WeiterlesenAndroid Basteln C und C++ Chaos Datenbanken Docker dWb+ ESP Wifi Garten Geo Go GUI Gui Hardware Java Java. Komponenten Jupyter JupyterBinder Komponenten Links Linux Markdown Markup Music Numerik OpenSource PKI-X.509-CA Präsentationen Python QBrowser Rants Raspi Revisited Security Software-Test sQLshell TeleGrafana Verschiedenes Video Virtualisierung Windows Upcoming...
Ich habe mich mit der Idee schon länger getragen: Nochmal einen Rundumschlag zu asymmetrischer Kryptographie zu machen. Dabei werde ich mich auf Demonstrationen der einzelnen Konzepte und Operationen mit Beispielcode konzentrieren und zu jedem der vorgestellten Konzepte mehr oder weniger ausführlich bezüglich der Einsatzszenarien und Vor- und Nachteile Stellung beziehen
WeiterlesenEs war wieder einmal an der Zeit, meine PKI zu warten...
WeiterlesenEine neue Version der sQLshell ist verfügbar!
WeiterlesenManche 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.