¿Son las llamadas estáticas de Java más o menos costosas que las llamadas no estáticas?

Respuestas:

74

Primero: no debería elegir entre estático y no estático en función del rendimiento.

Segundo: en la práctica, no hará ninguna diferencia. Hotspot puede optar por optimizar de manera que las llamadas estáticas sean más rápidas para un método y las llamadas no estáticas más rápidas para otro.

Tercero: gran parte de los mitos que rodean a estática versus no estática se basan en JVM muy antiguas (que no se acercan a la optimización que hace Hotspot), o en algunas trivialidades recordadas sobre C ++ (en las que una llamada dinámica usa un acceso más a la memoria que una llamada estática).

Luego
fuente
1
Tienes toda la razón en que no deberías preferir métodos estáticos basados ​​solo en esto. Sin embargo, en el caso de que los métodos estáticos se ajusten bien al diseño, es útil saber que son al menos tan rápidos, si no más rápidos, que los métodos de instancia y no deben descartarse en función del rendimiento.
Será el
2
@AaronDigulla -.- ¿y si te dijera que vine aquí porque estoy optimizando ahora mismo, no prematuramente, sino cuando realmente lo necesito? Asumiste que OP quiere optimizar prematuramente, pero sabes que este sitio es un poco global ... ¿verdad? No quiero ser grosero, pero no asumas cosas como esta la próxima vez.
Dalibor Filus
1
@DaliborFilus Necesito encontrar un equilibrio. El uso de métodos estáticos causa todo tipo de problemas, por lo que deben evitarse, especialmente cuando no sabe lo que está haciendo. En segundo lugar, la mayor parte del código "lento" se debe a un (mal) diseño, no a que el idioma elegido sea lento. Si su código es lento, los métodos estáticos probablemente no lo guardarán a menos que sus métodos de llamada no hagan absolutamente nada . En la mayoría de los casos, el código de los métodos empequeñecerá la sobrecarga de la llamada.
Aaron Digulla
6
Voto en contra. Esto no responde a la pregunta. La pregunta se refería a los beneficios de rendimiento. No pidió opiniones sobre los principios de diseño.
Colm Bhandal
4
Si entrenara a un loro para que dijera "la opimación prematura es la raíz de todos los males", obtendría 1000 votos de personas que saben tanto sobre rendimiento como el loro.
rghome
62

Cuatro años después...

Bien, con la esperanza de resolver esta pregunta de una vez y para siempre, he escrito un punto de referencia que muestra cómo los diferentes tipos de llamadas (virtuales, no virtuales, estáticas) se comparan entre sí.

Lo ejecuté en ideone , y esto es lo que obtuve:

(Es mejor un número mayor de iteraciones).

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

Como se esperaba, las llamadas a métodos virtuales son las más lentas, las llamadas a métodos no virtuales son más rápidas y las llamadas a métodos estáticos son incluso más rápidas.

Lo que no esperaba era que las diferencias fueran tan pronunciadas: las llamadas a métodos virtuales se midieron para que se ejecutaran a menos de la mitad de la velocidad de las llamadas a métodos no virtuales, que a su vez se midieron para que se ejecutaran un 15% más lento que las llamadas estáticas. Eso es lo que muestran estas medidas; de hecho, las diferencias reales deben ser un poco más pronunciadas, ya que para cada llamada de método virtual, no virtual y estático, mi código de evaluación comparativa tiene una sobrecarga constante adicional de incrementar una variable entera, verificar una variable booleana y realizar un bucle si no es cierto.

Supongo que los resultados variarán de una CPU a otra y de JVM a JVM, así que pruébelo y vea lo que obtiene:

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

Vale la pena señalar que esta diferencia de rendimiento solo es aplicable al código que no hace nada más que invocar métodos sin parámetros. Cualquier otro código que tenga entre las invocaciones diluirá las diferencias, y esto incluye el paso de parámetros. En realidad, la diferencia del 15% entre las llamadas estáticas y no virtuales probablemente se explique en su totalidad por el hecho de que el thispuntero no tiene que pasarse al método estático. Por lo tanto, solo se necesitaría una cantidad bastante pequeña de código haciendo cosas triviales entre llamadas para que la diferencia entre los diferentes tipos de llamadas se diluya hasta el punto de no tener ningún impacto neto.

Además, las llamadas a métodos virtuales existen por una razón; tienen un propósito que cumplir y se implementan utilizando los medios más eficientes proporcionados por el hardware subyacente. (El conjunto de instrucciones de la CPU). Si, en su deseo de eliminarlos reemplazándolos con llamadas no virtuales o estáticas, termina teniendo que agregar hasta un ápice de código adicional para emular su funcionalidad, entonces la sobrecarga neta resultante está limitada para ser no menos, sino más. Posiblemente, mucho, mucho, insondablemente mucho, más.

Mike Nakis
fuente
7
'Virtual' es un término de C ++. No hay métodos virtuales en Java. Hay métodos ordinarios, que son polimórficos en tiempo de ejecución, y métodos estáticos o finales, que no lo son.
Zhenya
16
@levgen sí, para alguien cuyo punto de vista es tan estrecho como la descripción general oficial de alto nivel del idioma, es precisamente como usted dice. Pero, por supuesto, los conceptos de alto nivel se implementan utilizando mecanismos de bajo nivel bien establecidos que se inventaron mucho antes de que existiera java, y los métodos virtuales son uno de ellos. Si echas
Mike Nakis
13
Gracias por responder la pregunta sin hacer suposiciones sobre la optimización prematura. Gran respuesta.
vegemite4me
3
Sí, eso es exactamente lo que quise decir. De todos modos, acabo de ejecutar la prueba en mi máquina. Aparte de la fluctuación que puede esperar de un punto de referencia de este tipo, no hay ninguna diferencia de velocidad: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198en mi instalación de OpenJDK. FTR: Eso es incluso cierto si elimino el finalmodificador. Por cierto. Tuve que hacer el terminatecampo volatile, de lo contrario la prueba no terminó.
Marten
4
Para su información, me sale bastante sorprendentes resultados en un Nexus 5 con Android 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. No solo que OpenJDK en mi portátil logra realizar 40 veces más iteraciones, la prueba estática siempre tiene aproximadamente un 30% menos de rendimiento. Este podría ser un fenómeno específico de ART, porque obtengo un resultado esperado en una tableta con Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten
46

Bueno, las llamadas estáticas no se pueden anular (por lo que siempre son candidatas para la inserción) y no requieren ninguna verificación de nulidad. HotSpot realiza un montón de optimizaciones interesantes, por ejemplo, métodos que pueden negar estas ventajas, pero son posibles razones por las que una llamada estática puede ser más rápida.

Sin embargo, eso no debería afectar su diseño, el código de la manera más legible y natural, y solo debe preocuparse por este tipo de microoptimización si tiene una causa justa (lo que casi nunca lo hará).

Jon Skeet
fuente
Son posibles razones por las que una llamada estática puede ser más rápida. ¿Podría explicarme esas razones?
JavaTechnical
6
@JavaTechnical: La respuesta explica esas razones: sin anular (lo que significa que no necesita trabajar en la implementación para usar cada vez y puede en línea) y no necesita verificar si está llamando al método en un referencia nula.
Jon Skeet
6
@JavaTechnical: No entiendo. Le acabo de dar cosas que no necesitan ser calculadas / verificadas para métodos estáticos, junto con una oportunidad de inserción. No trabajar es un beneficio de rendimiento. ¿Qué queda por entender?
Jon Skeet
¿Las variables estáticas se recuperan más rápido que las variables no estáticas?
JavaTechnical
1
@JavaTechnical: Bueno, no hay que realizar ninguna verificación de nulidad, pero si el compilador JIT puede eliminar esa verificación (que será específica del contexto), no esperaría mucha diferencia. Cosas como si la memoria está en caché serían mucho más importantes.
Jon Skeet
18

Es específico del compilador / VM.

  • En teoría , una llamada estática se puede hacer un poco más eficiente porque no necesita realizar una búsqueda de función virtual, y también puede evitar la sobrecarga del parámetro oculto "this".
  • En la práctica , muchos compiladores optimizarán esto de todos modos.

Por lo tanto, probablemente no valga la pena preocuparse por ello a menos que haya identificado esto como un problema de rendimiento verdaderamente crítico en su aplicación. La optimización prematura es la raíz de todos los males, etc.

Sin embargo, he visto que esta optimización proporciona un aumento sustancial del rendimiento en la siguiente situación:

  • Método que realiza un cálculo matemático muy simple sin accesos a la memoria
  • El método se invoca millones de veces por segundo en un circuito interno cerrado
  • Aplicación vinculada a la CPU donde cada bit de rendimiento importa

Si lo anterior se aplica a usted, puede valer la pena probarlo.

También hay otra buena razón (¡y potencialmente incluso más importante!) Para usar un método estático: si el método realmente tiene semántica estática (es decir, lógicamente no está conectado a una instancia determinada de la clase), entonces tiene sentido hacerlo estático para reflejar este hecho. Los programadores experimentados de Java notarán entonces el modificador estático e inmediatamente pensarán "¡ajá! Este método es estático por lo que no necesita una instancia y presumiblemente no manipula el estado específico de la instancia". Entonces habrá comunicado la naturaleza estática del método de manera efectiva ...

mikera
fuente
14

Como han dicho los carteles anteriores: Esto parece una optimización prematura.

Sin embargo, hay una diferencia (una parte del hecho de que las invocaciones no estáticas requieren un empuje adicional de un objeto llamado en la pila de operandos):

Dado que los métodos estáticos no se pueden anular, no habrá búsquedas virtuales en tiempo de ejecución para una llamada de método estático. Esto puede resultar en una diferencia observable en algunas circunstancias.

La diferencia a nivel de código de bytes es que una llamada a un método no estático se realiza a través de INVOKEVIRTUAL, INVOKEINTERFACEo INVOKESPECIALmientras se realiza una llamada a un método estático INVOKESTATIC.

aioobe
fuente
2
Sin embargo, un método de instancia privada se invoca (al menos normalmente) mediante el uso, invokespecialya que no es virtual.
Mark Peters
Ah, interesante, solo podía pensar en constructores, ¡por eso lo omití! ¡Gracias! (respuesta de actualización)
aioobe
2
La JVM se optimizará si se crea una instancia de un tipo. Si B extiende A, y no se ha instanciado ninguna instancia de B, las llamadas a métodos en A no necesitarán una búsqueda de tabla virtual.
Steve Kuo
13

Es increíblemente poco probable que cualquier diferencia en el rendimiento de las llamadas estáticas frente a las no estáticas esté marcando una diferencia en su aplicación. Recuerde que "la optimización prematura es la raíz de todos los males".

DJClayworth
fuente
¡Fue Knuth! shreevatsa.wordpress.com/2008/05/16/…
ShreevatsaR
¿Podría explicar con más detalle qué significa "" la optimización prematura es la raíz de todos los males "."?
user2121
La pregunta era "¿Existe algún beneficio en el rendimiento de una forma u otra?", Y esto responde exactamente a esa pregunta.
DJClayworth
13

7 años después ...

No tengo un gran grado de confianza en los resultados que encontró Mike Nakis porque no abordan algunos problemas comunes relacionados con las optimizaciones de Hotspot. Instrumenté los puntos de referencia usando JMH y encontré que la sobrecarga de un método de instancia es de aproximadamente 0.75% en mi máquina frente a una llamada estática. Dada esa baja sobrecarga, creo que, excepto en las operaciones más sensibles a la latencia, podría decirse que no es la mayor preocupación en el diseño de una aplicación. Los resultados resumidos de mi punto de referencia JMH son los siguientes;

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

Puedes ver el código aquí en Github;

https://github.com/nfisher/svsi

El punto de referencia en sí es bastante simple, pero tiene como objetivo minimizar la eliminación de código muerto y el plegado constante. Es posible que haya otras optimizaciones que he pasado por alto / pasado por alto y es probable que estos resultados varíen según la versión de JVM y el sistema operativo.

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
fuente
1
Interés puramente académico aquí. Tengo curiosidad acerca de los posibles beneficios que este tipo de microoptimización podría tener en métricas que no sean ops/sprincipalmente en un entorno ART (por ejemplo, uso de memoria, tamaño de archivo .oat reducido, etc.). ¿Conoce alguna herramienta o forma relativamente simple en la que se pueda intentar comparar estas otras métricas?
Ryan Thomas
Hotspot se da cuenta de que no hay extensiones para InstanceSum en la ruta de clases. Intente agregar otra clase que amplíe InstanceSum y anule el método.
Milán
12

Para la decisión de si un método debe ser estático, el aspecto de rendimiento debe ser irrelevante. Si tiene un problema de rendimiento, hacer que muchos métodos sean estáticos no le salvará el día. Dicho esto, es casi seguro que los métodos estáticos no son más lentos que cualquier método de instancia, en la mayoría de los casos ligeramente más rápidos :

1.) Los métodos estáticos no son polimórficos, por lo que la JVM tiene menos decisiones que tomar para encontrar el código real para ejecutar. Este es un punto discutible en Age of Hotspot, ya que Hotspot optimizará las llamadas al método de instancia que tienen solo un sitio de implementación, por lo que realizarán lo mismo.

2.) Otra sutil diferencia es que los métodos estáticos obviamente no tienen ninguna referencia a "esto". Esto da como resultado un marco de pila una ranura más pequeño que el de un método de instancia con la misma firma y cuerpo ("esto" se coloca en la ranura 0 de las variables locales en el nivel de código de bytes, mientras que para los métodos estáticos se usa la ranura 0 para la primera parámetro del método).

Durandal
fuente
5

Puede haber una diferencia, y puede ir en cualquier dirección para cualquier fragmento de código en particular, y puede cambiar incluso con una versión menor de la JVM.

Definitivamente, esto es parte del 97% de pequeñas eficiencias que debe olvidarse .

Michael Borgwardt
fuente
2
Incorrecto. No puedes asumir nada. Podría ser un circuito cerrado requerido para una interfaz de usuario de front-end, lo que podría marcar una gran diferencia en lo "ágil" que es la interfaz de usuario. Por ejemplo, una búsqueda TableViewde millones de registros.
trilogía
0

En teoría, menos costoso.

La inicialización estática se realizará incluso si crea una instancia del objeto, mientras que los métodos estáticos no realizarán ninguna inicialización que se realiza normalmente en un constructor.

Sin embargo, no lo he probado.

Señor de poder
fuente
1
@R. Bemrose, ¿qué tiene que ver la inicialización estática con esta pregunta?
Kirk Woll
@Kirk Woll: Porque la inicialización estática se realiza la primera vez que se hace referencia a la clase ... incluso antes de la primera llamada al método estático.
Powerlord
@R. Bemrose, claro, ya que está cargando la clase en la VM para empezar. Parece un no-sequitor, en mi opinión.
Kirk Woll
0

Como señala Jon, los métodos estáticos no se pueden anular, por lo que simplemente invocar un método estático puede ser, en un tiempo de ejecución de Java suficientemente ingenuo, más rápido que invocar un método de instancia.

Pero luego, incluso asumiendo que está en el punto en el que le importa estropear su diseño para ahorrar unos pocos nanosegundos, eso solo trae otra pregunta: ¿necesitará que el método se anule a sí mismo? Si cambia su código para convertir un método de instancia en un método estático para ahorrar un nanosegundo aquí y allá, y luego da la vuelta e implementa su propio despachador además de eso, es casi seguro que el suyo será menos eficiente que el construido en su tiempo de ejecución de Java ya.

Conocido
fuente
-2

Me gustaría agregar a las otras excelentes respuestas aquí que también depende de su flujo, por ejemplo:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

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

Preste atención a que crea un nuevo objeto MyRowMapper por cada llamada.
En su lugar, sugiero usar aquí un 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
fuente