Grande differenza di velocità di metodi statici e non statici equivalenti


86

In questo codice, quando creo un oggetto nel mainmetodo e quindi chiamo il metodo degli oggetti: ff.twentyDivCount(i)(viene eseguito in 16010 ms), viene eseguito molto più velocemente rispetto a chiamarlo utilizzando questa annotazione: twentyDivCount(i)(viene eseguito in 59516 ms). Ovviamente, quando lo eseguo senza creare un oggetto, rendo il metodo statico, quindi può essere chiamato nel main.

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");
    }
}

EDIT: Finora sembra che macchine diverse producano risultati diversi, ma usando JRE 1.8. * È dove il risultato originale sembra essere riprodotto in modo coerente.


4
Come stai eseguendo il tuo benchmark? Scommetto che questo è un artefatto della JVM che non ha abbastanza tempo per ottimizzare il codice.
Patrick Collins,

2
Sembra che sia tempo sufficiente per JVM per compilare ed eseguire un OSR per il metodo principale come +PrintCompilation +PrintInliningmostra
Tagir Valeev

1
Avevo provato lo snippet di codice, ma non ho riscontrato alcuna differenza di fuso orario come ha detto Stabbz. Loro 56282 ms (utilizzando l'istanza) 54551 ms (come metodo statico).
Don Chakkappan

1
@PatrickCollins Cinque secondi devono essere sufficienti. L'ho riscritto un po 'in modo da poter misurare entrambi (viene avviata una JVM per variante). So che come benchmark è ancora imperfetto, ma è abbastanza convincente: 1457 ms STATIC vs 5312 ms NON_STATIC.
maaartinus

1
Non ho ancora approfondito la questione in dettaglio, ma questo potrebbe essere correlato: shipilev.net/blog/2015/black-magic-method-dispatch (forse Aleksey Shipilëv può illuminarci qui)
Marco13

Risposte:


72

Utilizzando JRE 1.8.0_45 ottengo risultati simili.

Indagine:

  1. l'esecuzione di java con le -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningopzioni VM mostra che entrambi i metodi vengono compilati e inline
  2. Guardando l'assembly generato per i metodi stessi non mostra differenze significative
  3. Una volta che vengono inseriti inline, tuttavia, l'assembly generato all'interno mainè molto diverso, con il metodo dell'istanza ottimizzato in modo più aggressivo, specialmente in termini di srotolamento del loop

Ho quindi eseguito di nuovo il test ma con diverse impostazioni di srotolamento del ciclo per confermare il sospetto sopra. Ho eseguito il tuo codice con:

  • -XX:LoopUnrollLimit=0 ed entrambi i metodi vengono eseguiti lentamente (simile al metodo statico con le opzioni predefinite).
  • -XX:LoopUnrollLimit=100 ed entrambi i metodi vengono eseguiti velocemente (simile al metodo di istanza con le opzioni predefinite).

In conclusione sembra che, con le impostazioni predefinite, il JIT dell'hotspot 1.8.0_45 non sia in grado di srotolare il ciclo quando il metodo è statico (anche se non sono sicuro del motivo per cui si comporta in questo modo). Altre JVM possono produrre risultati diversi.


Tra 52 e 71, il comportamento originale viene ripristinato (almeno sulla mia macchina, s. La mia risposta). Sembra che la versione statica fosse più grande di 20 unità, ma perché? Questo è strano.
maaartinus

3
@maaartinus Non sono nemmeno sicuro di cosa rappresenti esattamente quel numero - il documento è abbastanza evasivo: " Srotola i corpi del ciclo con il nodo di rappresentazione intermedia del compilatore del server conta meno di questo valore. Il limite utilizzato dal compilatore del server è una funzione di questo valore, non il valore effettivo . Il valore predefinito varia a seconda della piattaforma su cui è in esecuzione la JVM. "...
assylias

Nemmeno io lo so, ma la mia prima ipotesi è stata che i metodi statici diventino leggermente più grandi in qualsiasi unità e che siamo riusciti a trovare il punto in cui conta. Tuttavia, la differenza è piuttosto grande, quindi la mia ipotesi attuale è che la versione statica ottenga alcune ottimizzazioni che la rendono un po 'più grande. Non ho esaminato l'ASM generato.
maaartinus

33

Solo un'ipotesi non dimostrata basata su una risposta di Assylias.

La JVM utilizza una soglia per lo srotolamento del loop, che è qualcosa come 70. Per qualsiasi motivo, la chiamata statica è leggermente più grande e non viene srotolata.

Aggiorna i risultati

  • Con il LoopUnrollLimitsotto 52, entrambe le versioni sono lente.
  • Tra 52 e 71, solo la versione statica è lenta.
  • Sopra 71, entrambe le versioni sono veloci.

Questo è strano poiché la mia ipotesi era che la chiamata statica fosse solo leggermente più grande nella rappresentazione interna e l'OP avesse colpito uno strano caso. Ma la differenza sembra essere di circa 20, il che non ha senso.

 

-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

Per coloro che desiderano sperimentare, la mia versione può essere utile.


Il tempo di "1456 ms"? Se sì, perché dici che statico è lento?
Tony

@ Tony ho confuso NON_STATICe STATIC, ma la mia conclusione era giusta. Risolto ora, grazie.
maaartinus

0

Quando viene eseguito in modalità debug, i numeri sono gli stessi per l'istanza e per i casi statici. Ciò significa inoltre che JIT esita a compilare il codice in codice nativo nel caso statico allo stesso modo del caso del metodo di istanza.

Perché lo fa? È difficile da dire; probabilmente farebbe la cosa giusta se questa fosse un'applicazione più grande ...


"Perché lo fa? Difficile da dire, probabilmente farebbe la cosa giusta se questa fosse un'app più grande." O avresti solo uno strano problema di prestazioni che è troppo grande per eseguire effettivamente il debug. (E non è così difficile da dire. Puoi guardare l'assemblea che la JVM sputa come facevano gli assylias.)
tmyklebu

@tmyklebu Oppure abbiamo uno strano problema di prestazioni che non è necessario e costoso per eseguire il debug completo e ci sono soluzioni alternative facili. Alla fine, stiamo parlando di JIT qui, i suoi autori non sanno come si comporta esattamente in tutte le situazioni. :) Guarda le altre risposte, sono molto buone e molto vicine a spiegare il problema, ma finora nessuno sa perché esattamente questo stia accadendo.
Dragan Bozanovic

@DraganBozanovic: smette di essere "non necessario per eseguire il debug completo" quando causa problemi reali nel codice reale.
tmyklebu

0

Ho appena modificato leggermente il test e ho ottenuto i seguenti risultati:

Produzione:

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

NOTA

Mentre li stavo testando separatamente ho ottenuto ~ 52 sec per dinamico e ~ 200 sec per statico.

Questo è il programma:

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");
    }
}

Ho anche cambiato l'ordine del test in:

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

E ho capito:

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

Come vedi, se dinamico viene chiamato prima di statico, la velocità per statica diminuisce drasticamente.

Sulla base di questo benchmark:

Io ipotizzo che tutto dipende l'ottimizzazione JVM. quindi ti consiglio solo di seguire la regola pratica per l'uso di metodi statici e dinamici.

REGOLA DEL POLLICE:

Java: quando utilizzare metodi statici


"devi seguire la regola pratica per l'uso di metodi statici e dinamici." Qual è questa regola pratica? E da chi / cosa stai citando?
Weston

@weston mi dispiace non ho aggiunto il collegamento a cui stavo pensando :). thx
nafas

0

Vi preghiamo di provare:

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;
    }
}

Da 20273 ms a 23000+ ms, diversi per ogni corsa
Stabbz
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.