¿Existe algún beneficio en el rendimiento de una forma u otra? ¿Es específico del compilador / VM? Estoy usando Hotspot.
¿Existe algún beneficio en el rendimiento de una forma u otra? ¿Es específico del compilador / VM? Estoy usando Hotspot.
Respuestas:
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).
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 this
puntero 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.
VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198
en mi instalación de OpenJDK. FTR: Eso es incluso cierto si elimino el final
modificador. Por cierto. Tuve que hacer el terminate
campo volatile
, de lo contrario la prueba no terminó.
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
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á).
Es específico del compilador / VM.
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:
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 ...
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
, INVOKEINTERFACE
o INVOKESPECIAL
mientras se realiza una llamada a un método estático INVOKESTATIC
.
invokespecial
ya que no es virtual.
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".
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);
}
}
ops/s
principalmente 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?
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).
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 .
TableView
de millones de registros.
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.
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.
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);
};
};