Grande différence de vitesse des méthodes statiques et non statiques équivalentes

86

Dans ce code, lorsque je crée un objet dans la mainméthode, puis que j'appelle cette méthode d'objets: ff.twentyDivCount(i)(s'exécute en 16010 ms), elle s'exécute beaucoup plus rapidement que de l'appeler en utilisant cette annotation: twentyDivCount(i)(s'exécute en 59516 ms). Bien sûr, quand je l'exécute sans créer d'objet, je rends la méthode statique, donc elle peut être appelée dans le 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: Jusqu'à présent, il semble que différentes machines produisent des résultats différents, mais en utilisant JRE 1.8. * Est l'endroit où le résultat original semble être reproduit de manière cohérente.

Stabbz
la source
4
Comment gérez-vous votre benchmark? Je parie que c'est un artefact de la JVM n'ayant pas assez de temps pour optimiser le code.
Patrick Collins
2
Semble qu'il est assez de temps pour que JVM compile et exécute un OSR pour la méthode principale comme le +PrintCompilation +PrintInliningmontre
Tagir Valeev
1
J'avais essayé l'extrait de code, mais je n'obtiens pas de décalage horaire comme Stabbz l'a dit. Ils 56282ms (en utilisant l'instance) 54551ms (en tant que méthode statique).
Don Chakkappan
1
@PatrickCollins Cinq secondes doivent suffire. Je l'ai réécrit un peu pour que vous puissiez mesurer les deux (une JVM démarre par variante). Je sais que comme point de repère, c'est toujours imparfait, mais c'est assez convaincant: 1457 ms STATIC vs 5312 ms NON_STATIC.
maaartinus
1
Je n'ai pas encore étudié la question en détail, mais cela pourrait être lié: shipilev.net/blog/2015/black-magic-method-dispatch (peut-être qu'Aleksey Shipilëv peut nous éclairer ici)
Marco13

Réponses:

72

En utilisant JRE 1.8.0_45, j'obtiens des résultats similaires.

Enquête:

  1. l'exécution de java avec les -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningoptions VM montre que les deux méthodes sont compilées et intégrées
  2. L'examen de l'assemblage généré pour les méthodes elles-mêmes ne montre aucune différence significative
  3. Une fois qu'ils sont intégrés, cependant, l'assemblage généré à l'intérieur mainest très différent, la méthode d'instance étant optimisée de manière plus agressive, en particulier en termes de déroulement de boucle.

J'ai ensuite exécuté à nouveau votre test mais avec des paramètres de déroulement de boucle différents pour confirmer la suspicion ci-dessus. J'ai exécuté votre code avec:

  • -XX:LoopUnrollLimit=0 et les deux méthodes s'exécutent lentement (similaire à la méthode statique avec les options par défaut).
  • -XX:LoopUnrollLimit=100 et les deux méthodes s'exécutent rapidement (similaire à la méthode d'instance avec les options par défaut).

En conclusion, il semble qu'avec les paramètres par défaut, le JIT du hotspot 1.8.0_45 ne soit pas capable de dérouler la boucle lorsque la méthode est statique (bien que je ne sache pas pourquoi il se comporte de cette façon). D'autres JVM peuvent donner des résultats différents.

assylies
la source
Entre 52 et 71, le comportement d'origine est rétabli (du moins sur ma machine, à ma réponse). Il semble que la version statique était de 20 unités plus grande, mais pourquoi? Cela est étrange.
maaartinus
3
@maaartinus Je ne sais même pas ce que ce nombre représente exactement - la documentation est assez évasive: " Déroulez les corps de boucle avec le nombre de nœuds de représentation intermédiaire du compilateur serveur inférieur à cette valeur. La limite utilisée par le compilateur serveur est une fonction de cette valeur, pas la valeur réelle . La valeur par défaut varie en fonction de la plate-forme sur laquelle la machine
virtuelle Java
Je ne sais pas non plus, mais ma première hypothèse était que les méthodes statiques deviennent légèrement plus grandes dans toutes les unités et que nous avons atteint l'endroit où cela compte. Cependant, la différence est assez grande, donc je suppose que la version statique bénéficie de quelques optimisations qui la rendent un peu plus grande. Je n'ai pas regardé l'asm généré.
maaartinus
33

Juste une supposition non prouvée basée sur la réponse d'une assylie.

La JVM utilise un seuil pour le déroulement de la boucle, qui est quelque chose comme 70. Pour une raison quelconque, l'appel statique est légèrement plus grand et ne se déroule pas.

Mettre à jour les résultats

  • Avec le LoopUnrollLimitdans les 52 ci-dessous, les deux versions sont lentes.
  • Entre 52 et 71, seule la version statique est lente.
  • Au-dessus de 71, les deux versions sont rapides.

C'est étrange car je suppose que l'appel statique est juste légèrement plus grand dans la représentation interne et que l'OP a rencontré un cas étrange. Mais la différence semble être d'environ 20, ce qui n'a aucun sens.

 

-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

Pour ceux qui souhaitent expérimenter, ma version peut être utile.

maaartinus
la source
Si c'est le cas, pourquoi dites-vous que la statique est lente?
Tony
@Tony j'ai confondu NON_STATICet STATIC, mais ma conclusion était juste. Corrigé maintenant, merci.
maaartinus
0

Quand ceci est exécuté en mode débogage, les nombres sont les mêmes pour l'instance et les cas statiques. Cela signifie en outre que le JIT hésite à compiler le code en code natif dans le cas statique de la même manière que dans le cas de la méthode d'instance.

Pourquoi le fait-il? C'est dur à dire; il ferait probablement la bonne chose s'il s'agissait d'une application plus grande ...

Dragan Bozanovic
la source
"Pourquoi le fait-il? Difficile à dire, il ferait probablement la bonne chose si c'était une application plus grande." Ou vous auriez juste un problème de performances étrange qui est trop gros pour être débogué. (Et ce n'est pas si difficile à dire. Vous pouvez regarder l'assemblage que la JVM crache comme assylias l'a fait.)
tmyklebu
@tmyklebu Ou nous avons un problème de performances étrange qui est inutile et coûteux à déboguer complètement et il existe des solutions de contournement faciles. A la fin, on parle de JIT ici, ses auteurs ne savent pas comment il se comporte exactement dans toutes les situations. :) Regardez les autres réponses, elles sont très bonnes et très proches pour expliquer le problème, mais jusqu'à présent, personne ne sait exactement pourquoi cela se produit.
Dragan Bozanovic
@DraganBozanovic: Cela cesse d'être "inutile de déboguer complètement" quand cela cause de vrais problèmes dans le code réel.
tmyklebu
0

J'ai juste modifié légèrement le test et j'ai obtenu les résultats suivants:

Production:

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

REMARQUE

Pendant que je les testais séparément, j'ai eu ~ 52 sec pour dynamique et ~ 200 sec pour statique.

Voici le programme:

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

J'ai également changé l'ordre du test en:

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

Et j'ai ceci:

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

Comme vous le voyez, si dynamic est appelé avant static, la vitesse de statique a considérablement diminué.

Sur la base de ce benchmark:

Je suppose que tout dépend de l'optimisation JVM. donc je vous recommande simplement de suivre la règle de base pour l'utilisation de méthodes statiques et dynamiques.

REGLE DE POUCE:

Java: quand utiliser des méthodes statiques

nafas
la source
"vous devez suivre la règle de base pour l'utilisation de méthodes statiques et dynamiques." Quelle est cette règle de base? Et de qui / quoi citez-vous?
weston
@weston désolé, je n'ai pas ajouté le lien auquel je pensais :). thx
nafas
0

S'il vous plaît essayez:

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
la source
20273 ms à 23000+ ms, différent pour chaque exécution
Stabbz