While-Schleife schneller, solange die Abbruchbedingung nicht vom Schleifeninneren abhängt

mariane

Mitglied
Hallo,

eher durch Zufall bin auf folgendes Phänomen gestoßen, kann es mir nicht so recht erklären und möchte fragen, ob bekannt und was die Hintergründe sind / sein können ...

testA führt für den Wertebereich von Integer im Schnitt 5 Schleifendurchgänge aus und benötigt ca. 10000ms (auf meinem Rechner)

testB sieht zunächst etwas konfus aus, ist aber funktional mit testA identisch und demonstriert das Phänomen. Obgleich nun konstant 32 Schleifendurchgänge benötigt werden, werden nur 10ms benötigt, also Faktor 1000.

Wird testB "optimiert" (es geht nur um die Reduzierung der Schleifendurchgänge), indem im Else-Zweig die Dummy-Anweisung (++k,--k) durch ein return k ersetzt wird, erhöht sich die Laufzeit schlagartig auf ca. 15000ms, obgleich testB vom strukturellen Aufbau unverändert bleibt und nun wie testA im Schnitt nur noch 5 Schleifendurchgängen benötigt. Alternativ könnte man auch die While-Bedingung i++<32 durch n != 0 ersetzen, es hätte die selbe Auswirkung.

Wie kann das sein? Offenbar ist eine While-Schleife schneller, solange die Abbruchbedingung nicht vom Schleifeninneren abhängt.

Beste Grüße, mariane

Java:
public static int testA(int n)
{
    int k = 0;
    while( n != 0 ) {
        k++;
        n &= n << 1;
    }
    return k;
}
Java:
public static int testB(int n)
{
    int k = 0, i = 0;
    while( i++ < 32 ) {
        if( n != 0 ) {
            ++k;
        }
        else {
            // return k;
            ++k;
            --k;
        }
        n &= n << 1;
    }
    return k;
}
Java:
long start = System.currentTimeMillis();
for( int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++ ) {
    testA(i);
    // testB(i);
}
System.out.println((System.currentTimeMillis() - start));
 
Zuletzt bearbeitet:
Also bei mir läuft das ganze - mit JMH gebenchmarkt - so wie es soll:
testA - ca. 2,2 ns/op
testB - ca. 24,8 ns/op

Wenn das im Schnitt ~4 Durchläufe hat, dann müssen bei testB (32/4 = 8) ~8 mal soviele Operationen ausgeführt werden, bis die Schleife terminiert und das läuft so wie erwartet.
~4 Schleifendurchläufe laufen natürlich schneller als 32.

Bei solchen Benchmarks immer benchmark harness verwenden, sonst hast du eben JVM Optimierungen, GC, etc. mitgemessen.
 
Das ist sehr typisch. Java benutzt einen JustInTime (JIT)-Compiler und dieser optimiert sehr viel. Unter anderem kann er bei der zweiten Variante die Schleife wegoptimieren und einfach den Schleifencode 32 mal hintereinander ausführen (Loop unrolling). Zudem gibt es etliche Faktoren, die hier mit rein spielen, zum Beispiel, da die Bedingung unabhängig vom Code ist, lässt sie sich parallel zum Schleifencode testen, in zweiteren Fall muss dies sequenziell passieren. Außerdem werden solche k++;k--; auch sehr schnell wegoptimiert.

Zudem sind solche "Microbenchmarks" sehr fehleranfällig, weil dadurch viele Vorteile des JIT nicht genutzt werden. (Manchmal müssen Anweisungen sehr oft ausgeführt werden, damit der JIT die Notwendigkeit sieht, zu optimieren.) Es gibt gewisse Frameworks, die solche Benchmarks ermöglichen.

Gerne lasse ich mich verbessern, falls das was ich geschrieben habe nicht 100% korrekt ist, aber ganz falsch ist es nicht. 🙂
 

Neue Themen


Zurück
Oben