Grande diferença na velocidade de métodos estáticos e não estáticos equivalentes


86

Neste código, quando eu crio um objeto no mainmétodo e chamo esse método de objetos: ff.twentyDivCount(i)(executa em 16010 ms), ele é executado muito mais rápido do que chamá-lo usando esta anotação: twentyDivCount(i)(executa em 59516 ms). Claro, quando eu o executo sem criar um objeto, eu torno o método estático, para que ele possa ser chamado no 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: Até agora, parece que máquinas diferentes produzem resultados diferentes, mas usando o JRE 1.8. * É onde o resultado original parece ser reproduzido de forma consistente.


4
Como você está executando seu benchmark? Aposto que este é um artefato da JVM não tendo tempo suficiente para otimizar o código.
Patrick Collins

2
Parece que é tempo suficiente para a JVM compilar e executar um OSR para o método principal, como +PrintCompilation +PrintInliningmostra
Tagir Valeev

1
Eu tentei o trecho de código, mas não estou obtendo nenhuma diferença de tempo como Stabbz disse. Eles 56282ms (usando instância) 54551ms (como método estático).
Don Chakkappan

1
@PatrickCollins Cinco segundos devem bastar. Eu reescrevi um pouco para que você possa medir tanto (uma JVM é iniciada por variante). Eu sei que como um benchmark ainda é falho, mas é convincente o suficiente: 1457 ms STATIC vs 5312 ms NON_STATIC.
maaartinus

1
Ainda não investiguei a questão em detalhes, mas isso pode estar relacionado: shipilev.net/blog/2015/black-magic-method-dispatch (talvez Aleksey Shipilëv possa nos esclarecer aqui)
Marco13

Respostas:


72

Usando o JRE 1.8.0_45, obtenho resultados semelhantes.

Investigação:

  1. executando java com as -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningopções de VM mostra que ambos os métodos são compilados e embutidos
  2. Olhando para a montagem gerada para os próprios métodos não mostra nenhuma diferença significativa
  3. Uma vez que eles entram em linha, no entanto, a montagem gerada dentro mainé muito diferente, com o método de instância sendo otimizado de forma mais agressiva, especialmente em termos de desenrolamento de loop

Em seguida, executei seu teste novamente, mas com configurações de desenrolamento de loop diferentes para confirmar a suspeita acima. Eu executei seu código com:

  • -XX:LoopUnrollLimit=0 e ambos os métodos são executados lentamente (semelhante ao método estático com as opções padrão).
  • -XX:LoopUnrollLimit=100 e ambos os métodos são executados rapidamente (semelhante ao método de instância com as opções padrão).

Como conclusão, parece que, com as configurações padrão, o JIT do hotspot 1.8.0_45 não é capaz de desenrolar o loop quando o método é estático (embora eu não saiba por que ele se comporta dessa forma). Outras JVMs podem produzir resultados diferentes.


Entre 52 e 71, o comportamento original é restaurado (pelo menos na minha máquina, v. Minha resposta). Parece que a versão estática era 20 unidades maior, mas por quê? Isto é estranho.
maaartinus

3
@maaartinus Eu nem tenho certeza do que esse número representa exatamente - o documento é bastante evasivo: " Desenrole corpos de loop com contagem de nós de representação intermediária do compilador do servidor menor que este valor. O limite usado pelo compilador do servidor é uma função deste valor, não o valor real . O valor padrão varia com a plataforma na qual o JVM está sendo executado. "...
assylias

Nem eu sei, mas meu primeiro palpite foi que os métodos estáticos ficam um pouco maiores em quaisquer unidades e que atingimos o ponto onde é importante. No entanto, a diferença é muito grande, então meu palpite atual é que a versão estática recebe algumas otimizações que a tornam um pouco maior. Eu não olhei para o conjunto gerado.
maaartinus

33

Apenas uma suposição não comprovada com base em uma resposta de assilias.

A JVM usa um limite para o desenrolamento do loop, que é algo como 70. Por alguma razão, a chamada estática é um pouco maior e não é desenrolada.

Resultados de atualização

  • Com o LoopUnrollLimitabaixo de 52, ambas as versões são lentas.
  • Entre 52 e 71, apenas a versão estática é lenta.
  • Acima de 71, ambas as versões são rápidas.

Isso é estranho, pois meu palpite era que a chamada estática é apenas um pouco maior na representação interna e o OP atingiu um caso estranho. Mas a diferença parece ser de cerca de 20, o que não faz 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 aqueles que desejam experimentar, minha versão pode ser útil.


É o tempo de '1456 ms'? Em caso afirmativo, por que você diz que a estática é lenta?
Tony

@Tony eu confundi NON_STATICe STATIC, mas minha conclusão estava certa. Consertado agora, obrigado.
maaartinus

0

Quando isso é executado no modo de depuração, os números são os mesmos para a instância e os casos estáticos. Isso significa ainda que o JIT hesita em compilar o código para o código nativo no caso estático da mesma maneira que faz no caso do método de instância.

Por que isso acontece? É difícil dizer; provavelmente faria a coisa certa se este fosse um aplicativo maior ...


"Por que isso acontece? É difícil dizer, provavelmente faria a coisa certa se este fosse um aplicativo maior." Ou você apenas teria um problema de desempenho estranho que é grande demais para depurar. (E não é tão difícil dizer. Você pode olhar para a montagem que a JVM expele como os assílias fizeram.)
tmyklebu

@tmyklebu Ou temos um problema de desempenho estranho que é desnecessário e caro para depurar totalmente e existem soluções fáceis. Afinal, estamos falando de JIT aqui, seus autores não sabem exatamente como ele se comporta em todas as situações. :) Olha as outras respostas, elas são muito boas e muito próximas para explicar o assunto, mas até agora ninguém sabe exatamente porque isso está acontecendo.
Dragan Bozanovic

@DraganBozanovic: Deixa de ser "desnecessário depurar totalmente" quando causa problemas reais no código real.
tmyklebu

0

Acabei de ajustar um pouco o teste e obtive os seguintes resultados:

Resultado:

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

NOTA

Enquanto os testava separadamente, obtive ~ 52 segundos para dinâmico e ~ 200 segundos para estático.

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

Eu também alterei a ordem do teste para:

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

E eu tenho isso:

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

Como você pode ver, se dynamic for chamado antes de static, a velocidade de static diminuiu drasticamente.

Com base nesta referência:

Minha hipótese é que tudo depende da otimização da JVM. portanto, apenas recomendo que você siga a regra geral para o uso de métodos estáticos e dinâmicos.

REGRA DO POLEGAR:

Java: quando usar métodos estáticos


"você segue a regra de ouro para o uso de métodos estáticos e dinâmicos." Qual é esta regra prática? E de quem / o que você está citando?
Weston

@weston, desculpe por não ter adicionado o link que estava pensando :). thx
nafas

0

Tente por favor:

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

20.273 ms a mais de 23.000 ms, diferente para cada execução
Stabbz de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.