As chamadas estáticas Java são mais ou menos caras do que as chamadas não estáticas?

95

Existe algum benefício de desempenho de uma forma ou de outra? É específico do compilador / VM? Estou usando o Hotspot.

Andy Faibishenko
fonte

Respostas:

74

Primeiro: você não deve escolher entre estático e não estático com base no desempenho.

Segundo: na prática, não fará nenhuma diferença. O ponto de acesso pode escolher otimizar de maneiras que tornem as chamadas estáticas mais rápidas para um método e as chamadas não estáticas mais rápidas para outro.

Terceiro: muitos dos mitos que cercam estático versus não estático são baseados em JVMs muito antigos (que não faziam nada perto da otimização que o Hotspot faz) ou em algumas curiosidades sobre C ++ (em que uma chamada dinâmica usa mais um acesso de memória do que uma chamada estática).

Anon
fonte
1
Você está absolutamente certo de que não deve preferir métodos estáticos baseados apenas nisso. No entanto, no caso de os métodos estáticos se ajustarem bem ao projeto, é útil saber que eles são pelo menos tão rápidos, senão mais rápidos que os métodos de instância e não devem ser descartados com base no desempenho.
Será
2
@AaronDigulla -.- e se eu dissesse que vim aqui porque estou otimizando agora, não prematuramente, mas quando realmente preciso? Você presumiu que o OP quer otimizar prematuramente, mas você sabe que este site é meio global ... Certo? Não quero ser rude, mas, por favor, não pense em coisas assim da próxima vez.
Dalibor Filus 01 de
1
@DaliborFilus, preciso encontrar um equilíbrio. Usar métodos estáticos causa todos os tipos de problemas, então eles devem ser evitados, especialmente quando você não sabe o que está fazendo. Em segundo lugar, a maior parte do código "lento" se deve a um design (ruim), não porque a linguagem de escolha seja lenta. Se seu código for lento, os métodos estáticos provavelmente não o salvarão, a menos que seus métodos de chamada não façam absolutamente nada . Na maioria dos casos, o código nos métodos diminuirá a sobrecarga da chamada.
Aaron Digulla
6
Votos negativos. Isso não responde à pergunta. A pergunta feita sobre os benefícios de desempenho. Não pediu opiniões sobre os princípios do design.
Colm Bhandal
4
Se eu treinasse um papagaio para dizer "a opimização prematura é a raiz de todo o mal", receberia 1000 votos de pessoas que sabem tanto sobre desempenho quanto o papagaio.
rghome
62

Quatro anos depois...

Ok, na esperança de resolver essa questão de uma vez por todas, eu escrevi um benchmark que mostra como os diferentes tipos de chamadas (virtuais, não virtuais, estáticas) se comparam entre si.

Eu executei em um único , e isto é o que consegui:

(Quanto maior o número de iterações, melhor.)

    Success time: 3.12 memory: 320576 signal:0
  Name          |  Iterations
    VirtualTest |  128009996
 NonVirtualTest |  301765679
     StaticTest |  352298601
Done.

Como esperado, as chamadas de métodos virtuais são as mais lentas, as chamadas de métodos não virtuais são mais rápidas e as chamadas de métodos estáticos são ainda mais rápidas.

O que eu não esperava era que as diferenças fossem tão pronunciadas: chamadas de método virtual foram medidas para rodar a menos da metade da velocidade de chamadas de método não virtuais, que por sua vez foram medidas para rodar 15% mais lento do que chamadas estáticas. É isso que essas medidas mostram; as diferenças reais devem ser um pouco mais pronunciadas, pois para cada chamada de método virtual, não virtual e estático, meu código de benchmarking tem uma sobrecarga constante adicional de incremento de uma variável inteira, verificação de uma variável booleana e loop se não for verdadeiro.

Suponho que os resultados variam de CPU para CPU e de JVM para JVM, então experimente e veja o que você obtém:

import java.io.*;

class StaticVsInstanceBenchmark
{
    public static void main( String[] args ) throws Exception
    {
        StaticVsInstanceBenchmark program = new StaticVsInstanceBenchmark();
        program.run();
    }

    static final int DURATION = 1000;

    public void run() throws Exception
    {
        doBenchmark( new VirtualTest( new ClassWithVirtualMethod() ), 
                     new NonVirtualTest( new ClassWithNonVirtualMethod() ), 
                     new StaticTest() );
    }

    void doBenchmark( Test... tests ) throws Exception
    {
        System.out.println( "  Name          |  Iterations" );
        doBenchmark2( devNull, 1, tests ); //warmup
        doBenchmark2( System.out, DURATION, tests );
        System.out.println( "Done." );
    }

    void doBenchmark2( PrintStream printStream, int duration, Test[] tests ) throws Exception
    {
        for( Test test : tests )
        {
            long iterations = runTest( duration, test );
            printStream.printf( "%15s | %10d\n", test.getClass().getSimpleName(), iterations );
        }
    }

    long runTest( int duration, Test test ) throws Exception
    {
        test.terminate = false;
        test.count = 0;
        Thread thread = new Thread( test );
        thread.start();
        Thread.sleep( duration );
        test.terminate = true;
        thread.join();
        return test.count;
    }

    static abstract class Test implements Runnable
    {
        boolean terminate = false;
        long count = 0;
    }

    static class ClassWithStaticStuff
    {
        static int staticDummy;
        static void staticMethod() { staticDummy++; }
    }

    static class StaticTest extends Test
    {
        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                ClassWithStaticStuff.staticMethod();
            }
        }
    }

    static class ClassWithVirtualMethod implements Runnable
    {
        int instanceDummy;
        @Override public void run() { instanceDummy++; }
    }

    static class VirtualTest extends Test
    {
        final Runnable runnable;

        VirtualTest( Runnable runnable )
        {
            this.runnable = runnable;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                runnable.run();
            }
        }
    }

    static class ClassWithNonVirtualMethod
    {
        int instanceDummy;
        final void nonVirtualMethod() { instanceDummy++; }
    }

    static class NonVirtualTest extends Test
    {
        final ClassWithNonVirtualMethod objectWithNonVirtualMethod;

        NonVirtualTest( ClassWithNonVirtualMethod objectWithNonVirtualMethod )
        {
            this.objectWithNonVirtualMethod = objectWithNonVirtualMethod;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                objectWithNonVirtualMethod.nonVirtualMethod();
            }
        }
    }

    static final PrintStream devNull = new PrintStream( new OutputStream() 
    {
        public void write(int b) {}
    } );
}

É importante notar que essa diferença de desempenho só se aplica ao código que não faz nada além de invocar métodos sem parâmetros. Qualquer outro código que você tenha entre as invocações diluirá as diferenças, e isso inclui a passagem de parâmetros. Na verdade, a diferença de 15% entre chamadas estáticas e não virtuais é provavelmente explicada por completo pelo fato de que o thisponteiro não precisa ser passado para o método estático. Portanto, seria necessário apenas uma pequena quantidade de código fazendo coisas triviais entre as chamadas para que a diferença entre os diferentes tipos de chamadas fosse diluída a ponto de não ter nenhum impacto na rede.

Além disso, as chamadas de método virtual existem por um motivo; eles têm um propósito a servir e são implementados usando os meios mais eficientes fornecidos pelo hardware subjacente. (O conjunto de instruções da CPU.) Se, no seu desejo de eliminá-los substituindo-os por chamadas não virtuais ou estáticas, você acabar tendo que adicionar até um iota de código extra para emular sua funcionalidade, então sua sobrecarga líquida resultante é limitada ser não menos, mas mais. Muito possivelmente, muito, muito, incomensuravelmente muito, mais.

Mike Nakis
fonte
7
'Virtual' é um termo C ++. Não existem métodos virtuais em Java. Existem métodos comuns, que são polimórficos em tempo de execução, e métodos estáticos ou finais, que não são.
Zhenya,
16
@levgen sim, para alguém cujo ponto de vista é tão estreito quanto a visão geral oficial de alto nível da língua, é exatamente como você diz. Mas é claro que os conceitos de alto nível são implementados usando mecanismos de baixo nível bem estabelecidos que foram inventados muito antes de o java existir, e os métodos virtuais são um deles. Se você der uma pequena olhada sob o capô, verá imediatamente que é assim: docs.oracle.com/javase/specs/jvms/se7/html/…
Mike Nakis
13
Obrigado por responder à pergunta sem fazer suposições sobre a otimização prematura. Ótima resposta.
vegemite4me
3
Sim, foi exatamente isso que eu quis dizer. De qualquer forma, acabei de fazer o teste na minha máquina. Além do tremor que você pode esperar de tal benchmark, não há nenhuma diferença na velocidade: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198na minha instalação do OpenJDK. FTR: Isso é verdade mesmo se eu remover o finalmodificador. Btw. Tive que fazer o terminatecampo volatile, senão a prova não acabou.
Marten,
4
FYI, eu obter resultados bastante surpreendente em um Nexus 5 rodando Android 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Além do OpenJDK no meu notebook conseguir realizar 40x mais iterações, o teste estático sempre tem cerca de 30% menos throughput. Este pode ser um fenômeno específico da ART, porque obtenho um resultado esperado em um tablet Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten,
46

Bem, as chamadas estáticas não podem ser substituídas (portanto, são sempre candidatas a inlining) e não exigem nenhuma verificação de nulidade. O HotSpot faz um monte de otimizações legais para métodos de instância que podem negar essas vantagens, mas são possíveis razões pelas quais uma chamada estática pode ser mais rápida.

No entanto, isso não deve afetar seu design - código da forma mais legível e natural - e apenas se preocupar com esse tipo de microotimização se tiver uma causa justa (o que você quase nunca terá).

Jon Skeet
fonte
Eles são possíveis motivos pelos quais uma chamada estática pode ser mais rápida. Você poderia me explicar esses motivos?
JavaTechnical de
6
@JavaTechnical: A resposta explica esses motivos - sem substituição (o que significa que você não precisa trabalhar na implementação para usar a cada vez e pode inline) e não precisa verificar se está chamando o método em um referência nula.
Jon Skeet
6
@JavaTechnical: Não entendo. Acabei de dar a você coisas que não precisam ser computadas / verificadas para métodos estáticos, junto com uma oportunidade inlining. Não trabalhar é um benefício de desempenho. O que falta entender?
Jon Skeet de
As variáveis ​​estáticas são recuperadas mais rapidamente do que as variáveis ​​não estáticas?
JavaTechnical de
1
@JavaTechnical: Bem, não há verificação de nulidade a ser realizada - mas se o compilador JIT puder remover essa verificação (que será específica do contexto), eu não esperaria muita diferença. Coisas como se a memória está no cache seriam muito mais importantes.
Jon Skeet de
18

É específico do compilador / VM.

  • Em teoria , uma chamada estática pode ser ligeiramente mais eficiente porque não precisa fazer uma pesquisa de função virtual e também pode evitar a sobrecarga do parâmetro oculto "this".
  • Na prática , muitos compiladores irão otimizar isso de qualquer maneira.

Portanto, provavelmente não vale a pena se preocupar com isso, a menos que você tenha identificado isso como um problema de desempenho verdadeiramente crítico em seu aplicativo. A otimização prematura é a raiz de todos os males, etc ...

No entanto , vi essa otimização dar um aumento substancial de desempenho na seguinte situação:

  • Método que realiza um cálculo matemático muito simples, sem acessos à memória
  • Método sendo chamado milhões de vezes por segundo em um loop interno estreito
  • Aplicativo vinculado à CPU onde cada bit de desempenho é importante

Se o acima se aplica a você, pode valer a pena testar.

Há também uma outra razão boa (e potencialmente ainda mais importante!) Para usar um método estático - se o método realmente tiver semântica estática (ou seja, logicamente não está conectado a uma determinada instância da classe), então faz sentido torná-lo estático para refletir esse fato. Os programadores Java experientes então notarão o modificador estático e pensarão imediatamente "aha! Este método é estático, então não precisa de uma instância e presumivelmente não manipula o estado específico da instância". Portanto, você comunicou a natureza estática do método de forma eficaz ....

Mikera
fonte
14

Como já disseram os pôsteres anteriores: Parece uma otimização prematura.

No entanto, há uma diferença (parte do fato de que as invocações não estáticas exigem um envio adicional de um objeto callee na pilha de operandos):

Como os métodos estáticos não podem ser substituídos, não haverá nenhuma pesquisa virtual no tempo de execução para uma chamada de método estático. Isso pode resultar em uma diferença observável em algumas circunstâncias.

A diferença em um nível de byte-code é que uma chamada de método não-estático é feito através de INVOKEVIRTUAL, INVOKEINTERFACEou INVOKESPECIALdurante uma chamada de método estático é feito através de INVOKESTATIC.

aioobe
fonte
2
Um método de instância privada, entretanto, é (pelo menos normalmente) invocado usando, invokespecialuma vez que não é virtual.
Mark Peters
Ah, interessante, eu só conseguia pensar em construtores, por isso omiti! Obrigado! (atualizando a resposta)
aioobe
2
A JVM otimizará se houver um tipo instanciado. Se B estende A, e nenhuma instância de B foi instanciada, as chamadas de método em A não precisarão de uma consulta de tabela virtual.
Steve Kuo
13

É inacreditavelmente improvável que qualquer diferença no desempenho de chamadas estáticas em relação às não estáticas esteja fazendo diferença em seu aplicativo. Lembre-se de que "a otimização prematura é a raiz de todos os males".

DJClayworth
fonte
poderia explicar melhor o que significa "" otimização prematura é a raiz de todos os males "."?
user2121
A pergunta era "Existe algum benefício de desempenho de uma forma ou de outra?", E isso responde exatamente a essa pergunta.
DJClayworth de
13

7 anos depois ...

Não tenho muita confiança nos resultados que Mike Nakis encontrou porque eles não abordam alguns problemas comuns relacionados às otimizações de pontos de acesso. Eu instrumentei benchmarks usando JMH e descobri que a sobrecarga de um método de instância é de cerca de 0,75% na minha máquina em comparação com uma chamada estática. Considerando a baixa sobrecarga, acho que, exceto nas operações mais sensíveis à latência, essa não é, sem dúvida, a maior preocupação no design de um aplicativo. Os resultados resumidos do meu benchmark JMH são os seguintes;

java -jar target/benchmark.jar

# -- snip --

Benchmark                        Mode  Cnt          Score         Error  Units
MyBenchmark.testInstanceMethod  thrpt  200  414036562.933 ± 2198178.163  ops/s
MyBenchmark.testStaticMethod    thrpt  200  417194553.496 ± 1055872.594  ops/s

Você pode olhar o código aqui no Github;

https://github.com/nfisher/svsi

O benchmark em si é bastante simples, mas visa minimizar a eliminação de código morto e o dobramento constante. Possivelmente, há outras otimizações que perdi / negligenciei e esses resultados provavelmente variam de acordo com a versão da JVM e o SO.

package ca.junctionbox.svsi;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

class InstanceSum {
    public int sum(final int a, final int b) {
        return a + b;
    }
}

class StaticSum {
    public static int sum(final int a, final int b) {
        return a + b;
    }
}

public class MyBenchmark {
    private static final InstanceSum impl = new InstanceSum();

    @State(Scope.Thread)
    public static class Input {
        public int a = 1;
        public int b = 2;
    }

    @Benchmark
    public void testStaticMethod(Input i, Blackhole blackhole) {
        int sum = StaticSum.sum(i.a, i.b);
        blackhole.consume(sum);
    }

    @Benchmark
    public void testInstanceMethod(Input i, Blackhole blackhole) {
        int sum = impl.sum(i.a, i.b);
        blackhole.consume(sum);
    }
}
Nathan
fonte
1
Interesse puramente acadêmico aqui. Estou curioso sobre os benefícios potenciais que esse tipo de ops/smicrootimização pode ter em métricas, exceto principalmente em um ambiente ART (por exemplo, uso de memória, tamanho de arquivo reduzido .oat, etc.). Você conhece alguma ferramenta / maneira relativamente simples de tentar avaliar essas outras métricas?
Ryan Thomas
O hotspot descobre que não há extensões para InstanceSum no classpath. Tente adicionar outra classe que estenda InstanceSum e substitua o método.
Milão
12

Para a decisão se um método deve ser estático, o aspecto do desempenho deve ser irrelevante. Se você tiver um problema de desempenho, tornar muitos métodos estáticos não vai salvar o dia. Dito isso, os métodos estáticos quase certamente não são mais lentos do que qualquer método de instância, na maioria dos casos ligeiramente mais rápidos :

1.) Os métodos estáticos não são polimórficos, portanto, a JVM tem menos decisões a tomar para encontrar o código real a ser executado. Este é um ponto discutível no Age of Hotspot, uma vez que o Hotspot otimizará chamadas de método de instância que têm apenas um site de implementação, para que executem o mesmo.

2.) Outra diferença sutil é que os métodos estáticos obviamente não têm nenhuma referência "esta". Isso resulta em um quadro de pilha um slot menor do que o de um método de instância com a mesma assinatura e corpo ("this" é colocado no slot 0 das variáveis ​​locais no nível de bytecode, enquanto para métodos estáticos o slot 0 é usado para o primeiro parâmetro do método).

Durandal
fonte
5

Pode haver uma diferença, e pode ser de qualquer maneira para qualquer parte específica do código, e pode mudar até mesmo com um pequeno release da JVM.

Isso é definitivamente parte dos 97% de pequenas eficiências que você deve esquecer .

Michael Borgwardt
fonte
2
Errado. Você não pode presumir nada. Pode ser um loop apertado necessário para uma IU de front-end, o que pode fazer uma grande diferença no quão "ágil" a IU é. Por exemplo, uma pesquisa em TableViewmilhões de registros.
trilogia
0

Em teoria, menos caro.

A inicialização estática será feita mesmo se você criar uma instância do objeto, enquanto os métodos estáticos não farão nenhuma inicialização normalmente feita em um construtor.

No entanto, não testei isso.

Powerlord
fonte
1
@R. Bemrose, o que a inicialização estática tem a ver com essa questão?
Kirk Woll
@Kirk Woll: Porque a inicialização estática é feita na primeira vez que a classe é referenciada ... incluindo antes da primeira chamada de método estático.
Powerlord
@R. Bemrose, claro, como está carregando a classe na VM para começar. Parece um não sequitor, IMO.
Kirk Woll
0

Como Jon observa, os métodos estáticos não podem ser substituídos, então simplesmente invocar um método estático pode ser - em um Java runtime suficientemente ingênuo - mais rápido do que invocar um método de instância.

Mas então, mesmo supondo que você esteja no ponto em que se preocupa em bagunçar seu design para economizar alguns nanossegundos, isso apenas levanta outra questão: você precisará substituir o método por si mesmo? Se você alterar seu código para tornar um método de instância em um método estático para economizar um nanossegundo aqui e ali, e então virar e implementar seu próprio despachante em cima disso, o seu quase certamente será menos eficiente do que aquele construído em seu Java runtime já.

Ken
fonte
-2

Eu gostaria de acrescentar às outras ótimas respostas aqui que também depende do seu fluxo, por exemplo:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, new MyRowMapper());
   };
};

Preste atenção ao criar um novo objeto MyRowMapper para cada chamada.
Em vez disso, sugiro usar aqui um campo estático.

Public class MyDao {

   private static RowMapper myRowMapper = new MyRowMapper();
   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, myRowMapper);
   };
};
Yair Zaslavsky
fonte