Großer Geschwindigkeitsunterschied zwischen äquivalenten statischen und nicht statischen Methoden

86

Wenn ich in diesem Code ein Objekt in der mainMethode erstelle und diese Objektmethode dann ff.twentyDivCount(i)aufrufe : (wird in 16010 ms ausgeführt), wird sie viel schneller ausgeführt als mit dieser Anmerkung: twentyDivCount(i)(wird in 59516 ms ausgeführt). Wenn ich es ausführe, ohne ein Objekt zu erstellen, mache ich die Methode natürlich statisch, damit sie hauptsächlich aufgerufen werden kann.

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {    // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way
                       // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

BEARBEITEN: Bisher scheinen verschiedene Maschinen unterschiedliche Ergebnisse zu liefern, aber bei Verwendung von JRE 1.8. * Scheint das ursprüngliche Ergebnis konsistent reproduziert zu werden.

Stabbz
quelle
4
Wie führen Sie Ihren Benchmark durch? Ich wette, dass dies ein Artefakt der JVM ist, die nicht genug Zeit hat, um den Code zu optimieren.
Patrick Collins
2
Es scheint genug Zeit für JVM zu sein, um ein OSR für die Hauptmethode zu kompilieren und durchzuführen, wie +PrintCompilation +PrintInlininggezeigt
Tagir Valeev
1
Ich hatte das Code-Snippet ausprobiert, aber ich bekomme keinen Zeitunterschied, wie Stabbz sagte. Sie 56282 ms (unter Verwendung der Instanz) 54551 ms (als statische Methode).
Don Chakkappan
1
@PatrickCollins Fünf Sekunden müssen ausreichen. Ich habe es ein wenig umgeschrieben, damit Sie beide messen können (eine JVM wird pro Variante gestartet). Ich weiß, dass es als Benchmark immer noch fehlerhaft ist, aber es überzeugt genug: 1457 ms STATIC vs 5312 ms NON_STATIC.
Maaartinus
1
Ich habe die Frage noch nicht im Detail untersucht, aber dies könnte verwandt sein: shipilev.net/blog/2015/black-magic-method-dispatch (vielleicht kann Aleksey Shipilëv uns hier aufklären)
Marco13

Antworten:

72

Mit JRE 1.8.0_45 erhalte ich ähnliche Ergebnisse.

Ermittlung:

  1. Das Ausführen von Java mit den -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningVM-Optionen zeigt, dass beide Methoden kompiliert und eingebunden werden
  2. Ein Blick auf die generierte Assembly für die Methoden selbst zeigt keinen signifikanten Unterschied
  3. Sobald sie inline sind, ist die generierte Assembly innerhalb von mainsehr unterschiedlich, wobei die Instanzmethode aggressiver optimiert wird, insbesondere im Hinblick auf das Abrollen der Schleife

Ich habe dann Ihren Test erneut ausgeführt, jedoch mit unterschiedlichen Einstellungen zum Abrollen der Schleife, um den oben genannten Verdacht zu bestätigen. Ich habe Ihren Code ausgeführt mit:

  • -XX:LoopUnrollLimit=0 und beide Methoden werden langsam ausgeführt (ähnlich der statischen Methode mit den Standardoptionen).
  • -XX:LoopUnrollLimit=100 und beide Methoden laufen schnell (ähnlich der Instanzmethode mit den Standardoptionen).

Zusammenfassend scheint es, dass die JIT von Hotspot 1.8.0_45 mit den Standardeinstellungen die Schleife nicht abrollen kann, wenn die Methode statisch ist (obwohl ich nicht sicher bin, warum sie sich so verhält). Andere JVMs können zu anderen Ergebnissen führen.

Assylien
quelle
Zwischen 52 und 71 wird das ursprüngliche Verhalten wiederhergestellt (zumindest auf meinem Computer, s. Meine Antwort). Es sieht so aus, als wäre die statische Version 20 Einheiten größer, aber warum? Das ist merkwürdig.
Maaartinus
3
@maaartinus Ich bin mir nicht einmal sicher, was diese Zahl genau darstellt - das Dokument ist ziemlich ausweichend: " Schleifenkörper mit Knoten mit mittlerer Darstellung des Server-Compilers abrollen, die kleiner als dieser Wert sind. Das vom Server-Compiler verwendete Limit ist eine Funktion dieses Werts. nicht der tatsächliche Wert . Der Standardwert variiert mit der Plattform, auf der die JVM ausgeführt wird. "...
Assylias
Ich weiß es auch nicht, aber meine erste Vermutung war, dass die statischen Methoden in allen Einheiten nur geringfügig größer werden und dass wir genau dort ankommen, wo es darauf ankommt. Der Unterschied ist jedoch ziemlich groß, so dass ich derzeit davon ausgehe, dass die statische Version einige Optimierungen erhält, die sie etwas größer machen. Ich habe mir den generierten Asm nicht angesehen.
Maaartinus
33

Nur eine unbewiesene Vermutung basiert auf der Antwort eines Assylias.

Die JVM verwendet einen Schwellenwert für das Abrollen von Schleifen, der etwa 70 beträgt. Aus irgendeinem Grund ist der statische Aufruf etwas größer und wird nicht abgewickelt.

Ergebnisse aktualisieren

  • Mit den LoopUnrollLimitin den folgenden 52 sind beide Versionen langsam.
  • Zwischen 52 und 71 ist nur die statische Version langsam.
  • Über 71 sind beide Versionen schnell.

Dies ist seltsam, da ich vermute, dass der statische Aufruf in der internen Darstellung nur geringfügig größer ist und das OP einen seltsamen Fall trifft. Aber der Unterschied scheint ungefähr 20 zu sein, was keinen Sinn ergibt.

 

-XX:LoopUnrollLimit=51
5400 ms NON_STATIC
5310 ms STATIC
-XX:LoopUnrollLimit=52
1456 ms NON_STATIC
5305 ms STATIC
-XX:LoopUnrollLimit=71
1459 ms NON_STATIC
5309 ms STATIC
-XX:LoopUnrollLimit=72
1457 ms NON_STATIC
1488 ms STATIC

Für diejenigen, die experimentieren möchten, kann meine Version nützlich sein.

Maaartinus
quelle
Ist '1456 ms' Zeit? Wenn ja, warum sagen Sie, dass statisch langsam ist?
Tony
@ Tony Ich habe verwirrt NON_STATICund STATIC, aber meine Schlussfolgerung war richtig. Jetzt behoben, danke.
Maaartinus
0

Wenn dies im Debug-Modus ausgeführt wird, sind die Zahlen für die Instanz und die statischen Fälle gleich. Dies bedeutet weiter, dass die JIT zögert, den Code im statischen Fall genauso wie im Fall der Instanzmethode zu nativem Code zu kompilieren.

Warum tut es das? Es ist schwer zu sagen; wahrscheinlich würde es das Richtige tun, wenn dies eine größere Anwendung wäre ...

Dragan Bozanovic
quelle
"Warum tut es das? Schwer zu sagen, wahrscheinlich würde es das Richtige tun, wenn dies eine größere App wäre." Oder Sie haben einfach ein seltsames Leistungsproblem, das zu groß ist, um es tatsächlich zu debuggen. (Und es ist nicht so schwer zu sagen. Sie können sich die Versammlung ansehen, die die JVM ausspuckt, wie es Assylias getan hat.)
tmyklebu
@tmyklebu Oder wir haben ein seltsames Leistungsproblem, dessen vollständiges Debuggen unnötig und teuer ist, und es gibt einfache Problemumgehungen. Am Ende sprechen wir hier über JIT, seine Autoren wissen nicht, wie es sich in allen Situationen genau verhält. :) Schauen Sie sich die anderen Antworten an, sie sind sehr gut und sehr nah dran, um das Problem zu erklären, aber bisher weiß niemand, warum genau dies geschieht.
Dragan Bozanovic
@DraganBozanovic: Es ist nicht mehr "unnötig, vollständig zu debuggen", wenn es echte Probleme in echtem Code verursacht.
tmyklebu
0

Ich habe den Test nur leicht angepasst und die folgenden Ergebnisse erhalten:

Ausgabe:

Dynamic Test:
465585120
232792560
232792560
51350 ms
Static Test:
465585120
232792560
232792560
52062 ms

HINWEIS

Während ich sie separat testete, bekam ich ~ 52 Sekunden für dynamisch und ~ 200 Sekunden für statisch.

Dies ist das Programm:

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {  // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    static int twentyDivCount2(int a) {
         int count = 0;
         for (int i = 1; i<21; i++) {

             if (a % i == 0) {
                 count++;
             }
         }
         return count;
    }

    public static void main(String[] args) {
        System.out.println("Dynamic Test: " );
        dynamicTest();
        System.out.println("Static Test: " );
        staticTest();
    }

    private static void staticTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        for (int i = start; i > 0; i--) {

            int temp = twentyDivCount2(i);

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }

    private static void dynamicTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

Ich habe auch die Reihenfolge des Tests geändert in:

public static void main(String[] args) {
    System.out.println("Static Test: " );
    staticTest();
    System.out.println("Dynamic Test: " );
    dynamicTest();
}

Und ich habe folgendes:

Static Test:
465585120
232792560
232792560
188945 ms
Dynamic Test:
465585120
232792560
232792560
50106 ms

Wie Sie sehen, hat sich die Geschwindigkeit für Statik drastisch verringert, wenn Dynamik vor Statik aufgerufen wird.

Basierend auf dieser Benchmark:

ich gehe davon aus, dass alles von der JVM-Optimierung abhängt. Daher empfehle ich Ihnen nur, die Faustregel für die Verwendung statischer und dynamischer Methoden einzuhalten.

FAUSTREGEL:

Java: Wann werden statische Methoden verwendet?

Nafas
quelle
"Sie müssen sich an die Faustregel für die Verwendung statischer und dynamischer Methoden halten." Was ist diese Faustregel? Und von wem / was zitierst du?
Weston
@weston Entschuldigung, ich habe den Link, an den ich dachte, nicht hinzugefügt :). thx
nafas
0

Bitte versuche:

public class ProblemFive {
    public static ProblemFive PROBLEM_FIVE = new ProblemFive();

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();
        int start = 500000000;
        int result = start;


        for (int i = start; i > 0; i--) {
            int temp = PROBLEM_FIVE.twentyDivCount(i); // faster way
            // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
                System.out.println((System.currentTimeMillis() - startT) + " ms");
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();
        System.out.println((end - startT) + " ms");
    }

    int twentyDivCount(int a) {  // change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i < 21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }
}
Chengpohi
quelle
20273 ms bis 23000+ ms, unterschiedlich für jeden Lauf
Stabbz