Gran diferencia en la velocidad de los métodos equivalentes estáticos y no estáticos

86

En este código, cuando creo un objeto en el mainmétodo y luego llamo a ese método de objetos: ff.twentyDivCount(i)(se ejecuta en 16010 ms), se ejecuta mucho más rápido que llamarlo con esta anotación: twentyDivCount(i)(se ejecuta en 59516 ms). Por supuesto, cuando lo ejecuto sin crear un objeto, hago que el método sea estático, por lo que se puede llamar en 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");
    }
}

EDITAR: Hasta ahora parece que diferentes máquinas producen resultados diferentes, pero usando JRE 1.8. * Es donde el resultado original parece reproducirse consistentemente.

Stabbz
fuente
4
¿Cómo está ejecutando su punto de referencia? Apostaría a que esto es un artefacto de la JVM que no tiene suficiente tiempo para optimizar el código.
Patrick Collins
2
Parece que es tiempo suficiente para que JVM compile y realice un OSR para el método principal como se +PrintCompilation +PrintInliningmuestra
Tagir Valeev
1
Probé el fragmento de código, pero no obtengo la diferencia de tiempo como dijo Stabbz. Son 56282ms (usando instancia) 54551ms (como método estático).
Don Chakkappan
1
@PatrickCollins Cinco segundos deben ser suficientes. Lo reescribí un poco para que pueda medir ambos (se inicia una JVM por variante). Sé que como punto de referencia todavía tiene fallas, pero es lo suficientemente convincente: 1457 ms STATIC vs 5312 ms NON_STATIC.
maaartinus
1
Aún no he investigado la pregunta en detalle, pero esto podría estar relacionado: shipilev.net/blog/2015/black-magic-method-dispatch (tal vez Aleksey Shipilëv pueda iluminarnos aquí)
Marco13

Respuestas:

72

Usando JRE 1.8.0_45 obtengo resultados similares.

Investigación:

  1. ejecutar java con las -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningopciones de VM muestra que ambos métodos se compilan y se insertan
  2. Observar el ensamblaje generado para los métodos en sí no muestra una diferencia significativa
  3. Sin embargo, una vez que se integran, el ensamblado generado dentro maines muy diferente, y el método de instancia se optimiza de manera más agresiva, especialmente en términos de desenrollado de bucles

Luego realicé su prueba nuevamente pero con diferentes configuraciones de desenrollado de bucle para confirmar la sospecha anterior. Ejecuté tu código con:

  • -XX:LoopUnrollLimit=0 y ambos métodos se ejecutan lentamente (similar al método estático con las opciones predeterminadas).
  • -XX:LoopUnrollLimit=100 y ambos métodos se ejecutan rápidamente (similar al método de instancia con las opciones predeterminadas).

Como conclusión, parece que, con la configuración predeterminada, el JIT del hotspot 1.8.0_45 no puede desenrollar el ciclo cuando el método es estático (aunque no estoy seguro de por qué se comporta de esa manera). Otras JVM pueden producir resultados diferentes.

Assylias
fuente
Entre 52 y 71, se restaura el comportamiento original (al menos en mi máquina, en mi respuesta). Parece que la versión estática era 20 unidades más grande, pero ¿por qué? Esto es extraño.
maaartinus
3
@maaartinus Ni siquiera estoy seguro de qué representa exactamente ese número: el documento es bastante evasivo: " Desenrollar cuerpos de bucle con el nodo de representación intermedia del compilador del servidor que cuenta menos que este valor. El límite utilizado por el compilador del servidor es una función de este valor, no el valor real . El valor predeterminado varía con la plataforma en la que se ejecuta la JVM. "...
assylias
Tampoco lo sé, pero mi primera suposición fue que los métodos estáticos se vuelven un poco más grandes en cualquier unidad y que damos en el clavo donde importa. Sin embargo, la diferencia es bastante grande, por lo que mi suposición actual es que la versión estática obtiene algunas optimizaciones que la hacen un poco más grande. No he mirado el asm generado.
maaartinus
33

Solo una suposición no probada basada en la respuesta de Assylias.

La JVM usa un umbral para el desenrollado de bucles, que es algo así como 70. Por alguna razón, la llamada estática es un poco más grande y no se desenrolla.

Actualizar resultados

  • Con el LoopUnrollLimit siguiente 52, ambas versiones son lentas.
  • Entre 52 y 71, solo la versión estática es lenta.
  • Por encima de 71, ambas versiones son rápidas.

Esto es extraño ya que mi conjetura fue que la llamada estática es solo un poco más grande en la representación interna y el OP encontró un caso extraño. Pero la diferencia parece ser de unos 20, lo que no tiene sentido.

 

-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

Para aquellos que deseen experimentar, mi versión puede ser útil.

maaartinus
fuente
¿Es el tiempo de 1456 ms? Si es así, ¿por qué dice que la estática es lenta?
Tony
@Tony me confundí NON_STATICy STATIC, pero mi conclusión fue correcta. Arreglado ahora, gracias.
maaartinus
0

Cuando se ejecuta en modo de depuración, los números son los mismos para la instancia y los casos estáticos. Eso significa además que el JIT vacila en compilar el código en código nativo en el caso estático de la misma manera que lo hace en el caso del método de instancia.

¿Por qué lo hace? Es difícil de decir; probablemente haría lo correcto si esta fuera una aplicación más grande ...

Dragan Bozanovic
fuente
"¿Por qué lo hace? Es difícil de decir, probablemente haría lo correcto si se tratara de una aplicación más grande". O simplemente tendría un problema de rendimiento extraño que es demasiado grande para depurarlo. (Y no es tan difícil de decir. Puedes mirar la asamblea que la JVM escupe como lo hizo Assylias.)
tmyklebu
@tmyklebu O tenemos un problema de rendimiento extraño que es innecesario y costoso de depurar por completo y hay soluciones fáciles. Al final, estamos hablando de JIT aquí, sus autores no saben cómo se comporta exactamente en todas las situaciones. :) Mira las otras respuestas, son muy buenas y muy cercanas para explicar el problema, pero hasta ahora nadie sabe exactamente por qué está sucediendo esto.
Dragan Bozanovic
@DraganBozanovic: Deja de ser "innecesario depurar completamente" cuando causa problemas reales en código real.
tmyklebu
0

Simplemente modifiqué la prueba ligeramente y obtuve los siguientes resultados:

Salida:

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

NOTA

Mientras los probaba por separado, obtuve ~ 52 segundos para dinámica y ~ 200 segundos para estática.

Este es el programa:

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

También cambié el orden de la prueba a:

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

Y tengo esto:

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

Como puede ver, si se llama dinámico antes que estático, la velocidad de estático disminuyó drásticamente.

Basado en este punto de referencia:

Me planteo la hipótesis de que todo depende de la optimización de la JVM. por lo tanto, solo le recomiendo que siga la regla general para el uso de métodos estáticos y dinámicos.

REGLA DE ORO:

Java: cuando usar métodos estáticos

nafas
fuente
"debe seguir la regla general para el uso de métodos estáticos y dinámicos". ¿Qué es esta regla de oro? ¿Y de quién / qué estás citando?
Weston
@weston lo siento, no agregué el enlace que estaba pensando :). thx
nafas
0

Por favor, inténtalo:

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
fuente
20273 ms a 23000+ ms, diferente para cada ejecución
Stabbz