C'è qualche vantaggio in termini di prestazioni in un modo o nell'altro? È specifico per il compilatore / VM? Sto usando Hotspot.
C'è qualche vantaggio in termini di prestazioni in un modo o nell'altro? È specifico per il compilatore / VM? Sto usando Hotspot.
Risposte:
Primo: non dovresti scegliere tra statico e non statico sulla base delle prestazioni.
Secondo: in pratica, non farà alcuna differenza. L'hotspot può scegliere di ottimizzare in modi che rendono le chiamate statiche più veloci per un metodo, le chiamate non statiche più veloci per un altro.
Terzo: gran parte dei miti che circondano statico e non statico si basano su JVM molto vecchie (che non si avvicinavano all'ottimizzazione di Hotspot) o su alcune curiosità ricordate su C ++ (in cui una chiamata dinamica utilizza un accesso in più alla memoria di una chiamata statica).
Quattro anni dopo...
Ok, nella speranza di risolvere questa domanda una volta per sempre, ho scritto un benchmark che mostra come i diversi tipi di chiamate (virtuali, non virtuali, statiche) si confrontano tra loro.
L'ho eseguito su ideone , e questo è quello che ho ottenuto:
(È meglio un numero maggiore di iterazioni).
Success time: 3.12 memory: 320576 signal:0
Name | Iterations
VirtualTest | 128009996
NonVirtualTest | 301765679
StaticTest | 352298601
Done.
Come previsto, le chiamate ai metodi virtuali sono le più lente, le chiamate ai metodi non virtuali sono più veloci e le chiamate ai metodi statici sono ancora più veloci.
Quello che non mi aspettavo era che le differenze fossero così pronunciate: le chiamate ai metodi virtuali sono state misurate per essere eseguite a meno della metà della velocità delle chiamate ai metodi non virtuali, che a loro volta sono state misurate per essere eseguite un intero 15% più lentamente delle chiamate statiche. Questo è ciò che mostrano queste misurazioni; le differenze effettive devono infatti essere leggermente più pronunciate, poiché per ogni chiamata al metodo virtuale, non virtuale e statico, il mio codice di benchmarking ha un sovraccarico costante aggiuntivo di incremento di una variabile intera, controllo di una variabile booleana e ciclo se non vero.
Suppongo che i risultati varieranno da CPU a CPU e da JVM a JVM, quindi provalo e guarda cosa ottieni:
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 notare che questa differenza di prestazioni è applicabile solo al codice che non fa altro che invocare metodi senza parametri. Qualunque altro codice tu abbia tra le invocazioni diluirà le differenze e questo include il passaggio di parametri. In realtà, la differenza del 15% tra chiamate statiche e non virtuali è probabilmente spiegata completamente dal fatto che il this
puntatore non deve essere passato al metodo statico. Quindi, ci vorrebbe solo una piccola quantità di codice che fa cose banali tra le chiamate per diluire la differenza tra i diversi tipi di chiamate al punto da non avere alcun impatto netto.
Inoltre, le chiamate ai metodi virtuali esistono per un motivo; hanno uno scopo da servire e sono implementati utilizzando i mezzi più efficienti forniti dall'hardware sottostante. (Il set di istruzioni della CPU.) Se, nel tuo desiderio di eliminarli sostituendoli con chiamate non virtuali o statiche, finisci per dover aggiungere una briciola di codice extra per emulare la loro funzionalità, allora il tuo overhead netto risultante è vincolato essere non meno, ma di più. Probabilmente, molto, molto, insondabilmente molto, di più.
VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198
sulla mia installazione di OpenJDK. FTR: Questo è vero anche se rimuovo il final
modificatore. Btw. Dovevo terminate
scendere in campo volatile
, altrimenti la prova non finiva.
VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170
. Non solo OpenJDK sul mio notebook riesce a eseguire 40 volte più iterazioni, il test statico ha sempre circa il 30% in meno di throughput. Questo potrebbe essere un fenomeno specifico di ART, perché ottengo un risultato atteso su un tablet Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Bene, le chiamate statiche non possono essere sovrascritte (quindi sono sempre candidate per l'inlining) e non richiedono alcun controllo di nullità. HotSpot esegue una serie di fantastiche ottimizzazioni per i metodi di istanza che potrebbero negare questi vantaggi, ma sono possibili motivi per cui una chiamata statica potrebbe essere più veloce.
Tuttavia, ciò non dovrebbe influire sul tuo design - codice nel modo più leggibile e naturale - e preoccuparti di questo tipo di microottimizzazione solo se hai una giusta causa (cosa che quasi mai ).
È specifico per compilatore / VM.
Quindi probabilmente non vale la pena preoccuparsi a meno che non si sia identificato come un problema di prestazioni veramente critico nella propria applicazione. L'ottimizzazione prematura è la radice di tutti i mali, ecc ...
Comunque ho già visto questa ottimizzazione dare un sostanziale aumento delle prestazioni nella seguente situazione:
Se quanto sopra si applica a te, potrebbe valere la pena testarlo.
C'è anche un'altra buona ragione (e potenzialmente ancora più importante!) Per usare un metodo statico - se il metodo ha effettivamente una semantica statica (cioè logicamente non è connesso a una data istanza della classe) allora ha senso renderlo statico per riflettere questo fatto. I programmatori Java esperti noteranno quindi il modificatore statico e penseranno immediatamente "aha! Questo metodo è statico quindi non necessita di un'istanza e presumibilmente non manipola lo stato specifico dell'istanza". Quindi avrai comunicato la natura statica del metodo in modo efficace ...
Come hanno detto i poster precedenti: questa sembra un'ottimizzazione prematura.
Tuttavia, c'è una differenza (a parte il fatto che le invocazioni non statiche richiedono un ulteriore push di un oggetto chiamato sullo stack di operandi):
Poiché i metodi statici non possono essere sovrascritti, non ci saranno ricerche virtuali in runtime per una chiamata al metodo statico. Ciò può comportare una differenza osservabile in alcune circostanze.
La differenza a livello di byte-codice è che una chiamata di metodo non statico viene fatto attraverso INVOKEVIRTUAL
, INVOKEINTERFACE
o INVOKESPECIAL
durante una chiamata metodo statico viene fatto attraverso INVOKESTATIC
.
invokespecial
poiché non è virtuale.
È incredibilmente improbabile che qualsiasi differenza nelle prestazioni delle chiamate statiche rispetto a quelle non statiche stia facendo la differenza nella vostra applicazione. Ricorda che "l'ottimizzazione prematura è la radice di tutti i mali".
7 anni dopo ...
Non ho una grande fiducia nei risultati che Mike Nakis ha trovato perché non risolvono alcuni problemi comuni relativi alle ottimizzazioni dell'hotspot. Ho strumentato i benchmark utilizzando JMH e ho riscontrato che l'overhead di un metodo di istanza è di circa lo 0,75% sulla mia macchina rispetto a una chiamata statica. Considerato il basso overhead, penso che, tranne nelle operazioni più sensibili alla latenza, non sia probabilmente la più grande preoccupazione nella progettazione di un'applicazione. I risultati di riepilogo del mio benchmark JMH sono i seguenti;
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
Puoi guardare il codice qui su Github;
https://github.com/nfisher/svsi
Il benchmark stesso è piuttosto semplice ma mira a ridurre al minimo l'eliminazione del codice morto e il ripiegamento costante. Probabilmente ci sono altre ottimizzazioni che ho perso / trascurato e questi risultati possono variare a seconda della versione e del sistema operativo di JVM.
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 in un ambiente ART (ad esempio, utilizzo della memoria, dimensioni ridotte del file .oat, ecc.). Conosci strumenti / modi relativamente semplici in cui si potrebbe tentare di confrontare queste altre metriche?
Per decidere se un metodo debba essere statico, l'aspetto delle prestazioni dovrebbe essere irrilevante. Se hai un problema di prestazioni, rendere statici molti metodi non salverà la giornata. Detto questo, i metodi statici quasi certamente non sono più lenti di qualsiasi metodo di istanza, nella maggior parte dei casi leggermente più veloce :
1.) I metodi statici non sono polimorfici, quindi la JVM ha meno decisioni da prendere per trovare il codice effettivo da eseguire. Questo è un punto controverso in Age of Hotspot, poiché Hotspot ottimizzerà le chiamate al metodo di istanza che hanno un solo sito di implementazione, quindi eseguiranno lo stesso.
2.) Un'altra sottile differenza è che i metodi statici ovviamente non hanno alcun riferimento a "questo". Ciò si traduce in uno stack frame uno slot più piccolo di quello di un metodo di istanza con la stessa firma e corpo ("this" viene messo nello slot 0 delle variabili locali a livello di bytecode, mentre per i metodi statici lo slot 0 viene utilizzato per il primo parametro del metodo).
Potrebbe esserci una differenza e potrebbe andare in entrambi i modi per qualsiasi particolare pezzo di codice e potrebbe cambiare anche con una versione minore della JVM.
Questo fa sicuramente parte del 97% delle piccole efficienze che dovresti dimenticare .
TableView
di milioni di record.
In teoria, meno costoso.
L'inizializzazione statica verrà eseguita anche se si crea un'istanza dell'oggetto, mentre i metodi statici non eseguiranno alcuna inizializzazione normalmente eseguita in un costruttore.
Tuttavia, non l'ho testato.
Come osserva Jon, i metodi statici non possono essere sovrascritti, quindi semplicemente invocare un metodo statico potrebbe essere - su un runtime Java sufficientemente ingenuo - più veloce che invocare un metodo di istanza.
Ma poi, anche supponendo che tu sia al punto in cui ti interessa rovinare il tuo progetto per risparmiare qualche nanosecondo, questo solleva solo un'altra domanda: avrai bisogno di un metodo che preveda te stesso? Se cambi il tuo codice per trasformare un metodo di istanza in un metodo statico per salvare un nanosecondo qua e là, e poi ti giri e implementi il tuo dispatcher in cima a quello, il tuo sarà quasi certamente meno efficiente di quello costruito già nel tuo runtime Java.
Vorrei aggiungere alle altre ottime risposte qui che dipende anche dal tuo flusso, ad esempio:
Public class MyDao {
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, new MyRowMapper());
};
};
Fai attenzione a creare un nuovo oggetto MyRowMapper per ogni chiamata.
Suggerisco invece di utilizzare qui un campo statico.
Public class MyDao {
private static RowMapper myRowMapper = new MyRowMapper();
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, myRowMapper);
};
};