L’uso della parola chiave final in Java migliora le prestazioni?

In Java vediamo molti posti in cui è ansible utilizzare la parola chiave final ma il suo utilizzo è raro.

Per esempio:

 String str = "abc"; System.out.println(str); 

Nel caso precedente, str può essere final ma questo è comunemente lasciato fuori.

Quando un metodo non verrà mai sovrascritto, possiamo utilizzare la parola chiave final. Allo stesso modo nel caso di una class che non sarà ereditata.

L’utilizzo della parola chiave final in alcuni o tutti questi casi migliora davvero le prestazioni? Se è così, allora come? Spiega per favore. Se l’uso corretto della final davvero importante per le prestazioni, quali abitudini dovrebbe sviluppare un programmatore Java per utilizzare al meglio la parola chiave?

Di solito no. Per i metodi virtuali, HotSpot tiene traccia del fatto che il metodo è stato sovrascritto ed è in grado di eseguire ottimizzazioni come l’inline supponendo che un metodo non sia stato sovrascritto, finché non carica una class che sovrascrive il metodo, a quel punto può annullare (o parzialmente annullare) quelle ottimizzazioni.

(Ovviamente, si presume che tu stia usando HotSpot, ma è di gran lunga la JVM più comune, quindi …)

A mio avviso, dovresti utilizzare la versione final basata su un design e una leggibilità chiari piuttosto che su motivi di prestazioni. Se si desidera modificare qualcosa per motivi di prestazioni, è necessario eseguire misurazioni appropriate prima di piegare il codice più chiaro fuori forma – in questo modo è ansible decidere se le prestazioni extra ottenute valgono la scarsa leggibilità / progettazione. (Nella mia esperienza non ne vale quasi la pena; YMMV.)

EDIT: Come sono stati menzionati i campi finali, vale la pena di ricordare che spesso sono comunque una buona idea, in termini di design chiaro. Cambiano anche il comportamento garantito in termini di visibilità cross-thread: dopo che un costruttore è stato completato, qualsiasi campo finale è garantito per essere visibile immediatamente in altri thread. Questo è probabilmente l’uso più comune della final nella mia esperienza, anche se come sostenitore della regola del “design per ereditarietà o proibizione” di Josh Bloch, dovrei probabilmente usare la final più spesso per le classi …

Risposta breve: non ti preoccupare!

Risposta lunga:

Quando parli di variabili locali finali, tieni presente che l’utilizzo della parola chiave final aiuterà il compilatore a ottimizzare il codice in modo statico , il che può portare a un codice più veloce. Ad esempio, le stringhe finali a + b nell’esempio seguente sono concatenate staticamente (al momento della compilazione).

 public class FinalTest { public static final int N_ITERATIONS = 1000000; public static String testFinal() { final String a = "a"; final String b = "b"; return a + b; } public static String testNonFinal() { String a = "a"; String b = "b"; return a + b; } public static void main(String[] args) { long tStart, tElapsed; tStart = System.currentTimeMillis(); for (int i = 0; i < N_ITERATIONS; i++) testFinal(); tElapsed = System.currentTimeMillis() - tStart; System.out.println("Method with finals took " + tElapsed + " ms"); tStart = System.currentTimeMillis(); for (int i = 0; i < N_ITERATIONS; i++) testNonFinal(); tElapsed = System.currentTimeMillis() - tStart; System.out.println("Method without finals took " + tElapsed + " ms"); } } 

Il risultato?

 Method with finals took 5 ms Method without finals took 273 ms 

Testato su Java Hotspot VM 1.7.0_45-b18.

Quindi, quanto è il miglioramento effettivo delle prestazioni? Non oso dire Nella maggior parte dei casi, probabilmente marginale (~ 270 nanosecondi in questo test sintetico perché la concatenazione delle stringhe è evitata del tutto - un caso raro), ma in un codice di utilità altamente ottimizzato potrebbe essere un fattore. In ogni caso la risposta alla domanda originale è sì, potrebbe migliorare le prestazioni, ma marginalmente nella migliore delle ipotesi .

Prestazioni a tempo di compilazione a parte, non ho trovato alcuna prova che l'uso della parola chiave final abbia qualche effetto misurabile sulle prestazioni.

Sì, può. Ecco un’istanza in cui la finale può migliorare le prestazioni:

La compilazione condizionale è una tecnica in cui le righe di codice non vengono compilate nel file di class in base a una particolare condizione. Questo può essere usato per rimuovere tonnellate di codice di debug in una build di produzione.

considerare quanto segue:

 public class ConditionalCompile { private final static boolean doSomething= false; if (doSomething) { // do first part. } if (doSomething) { // do second part. } if (doSomething) { // do third part. } if (doSomething) { // do finalization part. } } 

Convertendo l’attributo doSomething in un attributo finale, hai detto al compilatore che ogni volta che vede doSomething, dovrebbe sostituirlo con false secondo le regole di sostituzione in fase di compilazione. Il primo passaggio del compilatore cambia il codice in qualcosa del genere:

 public class ConditionalCompile { private final static boolean doSomething= false; if (false){ // do first part. } if (false){ // do second part. } if (false){ // do third part. } if (false){ // do finalization part. } } 

Una volta fatto, il compilatore ne dà un’altra occhiata e vede che ci sono istruzioni non raggiungibili nel codice. Dato che stai lavorando con un compilatore di alta qualità, non gli piacciono tutti quei codici byte non raggiungibili. Quindi li rimuove e finisci con questo:

 public class ConditionalCompile { private final static boolean doSomething= false; public static void someMethodBetter( ) { // do first part. // do second part. // do third part. // do finalization part. } } 

riducendo così qualsiasi codice eccessivo o qualsiasi controllo condizionale non necessario.

Modifica: ad esempio, prendiamo il seguente codice:

 public class Test { public static final void main(String[] args) { boolean x = false; if (x) { System.out.println("x"); } final boolean y = false; if (y) { System.out.println("y"); } if (false) { System.out.println("z"); } } } 

Quando compili questo codice con Java 8 e decompilando con javap -c Test.class otteniamo:

 public class Test { public Test(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."":()V 4: return public static final void main(java.lang.String[]); Code: 0: iconst_0 1: istore_1 2: iload_1 3: ifeq 14 6: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #22 // String x 11: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: iconst_0 15: istore_2 16: return } 

Possiamo notare che il codice compilato include solo la variabile non finale x . Ciò dimostra che le variabili finali hanno un impatto sulle prestazioni, almeno per questo semplice caso.

Secondo IBM – non fa per classi o metodi.

http://www.ibm.com/developerworks/java/library/j-jtp04223.html

Stai davvero chiedendo due (almeno) casi diversi:

  1. final per variabili locali
  2. final per metodi / classi

Jon Skeet ha già risposto 2). Circa 1):

Non penso che faccia la differenza; per le variabili locali, il compilatore può dedurre se la variabile è definitiva o meno (semplicemente controllando se è stata assegnata più volte). Quindi, se il compilatore desidera ottimizzare le variabili assegnate una sola volta, può farlo indipendentemente dal fatto che la variabile sia effettivamente dichiarata final o meno.

final potrebbe fare la differenza per i campi protetti / di class pubblica; è molto difficile per il compilatore scoprire se il campo viene impostato più di una volta, poiché potrebbe accadere da una class diversa (che potrebbe anche non essere stata caricata). Ma anche in quel caso la JVM potrebbe usare la tecnica descritta da Jon (ottimizzandola ottimisticamente, ripristina se una class viene caricata e cambia il campo).

In sintesi, non vedo alcuna ragione per cui dovrebbe aiutare le prestazioni. Quindi è improbabile che questo tipo di micro-ottimizzazione aiuti. Potresti provare a fare un benchmark per assicurarti, ma dubito che farà la differenza.

Modificare:

In realtà, secondo la risposta di Timo Westkämper, la final può migliorare le prestazioni per i campi di class in alcuni casi. Sono corretto.

Sono stupito che nessuno abbia effettivamente pubblicato un codice reale che è stato decompilato per dimostrare che c’è almeno una piccola differenza.

Per il riferimento questo è stato testato contro javac versione 8 , 9 e 10 .

Supponi questo metodo:

 public static int test() { /* final */ Object left = new Object(); Object right = new Object(); return left.hashCode() + right.hashCode(); } 

La compilazione di questo codice così com’è, produce lo stesso identico codice byte di quando sarebbe stato presente l’ final Object left = new Object(); ( final Object left = new Object(); ).

Ma questo:

 public static int test() { /* final */ int left = 11; int right = 12; return left + right; } 

produce:

  0: bipush 11 2: istore_0 3: bipush 12 5: istore_1 6: iload_0 7: iload_1 8: iadd 9: ireturn 

Lasciando la final per essere presente produce:

  0: bipush 12 2: istore_1 3: bipush 11 5: iload_1 6: iadd 7: ireturn 

Il codice è praticamente auto-esplicativo, nel caso in cui vi sia una costante di tempo di compilazione, verrà caricato direttamente sullo stack degli operandi (non verrà memorizzato nella matrice di variabili locali come nell’esempio precedente tramite bipush 12; istore_0; iload_0 ) – che tipo di ha senso dal momento che nessuno può cambiarlo.

D’altra parte perché nel secondo caso il compilatore non produce istore_0 ... iload_0 è al di là di me, non è come se lo slot 0 fosse usato in qualsiasi modo (potrebbe ridurre la matrice di variabili in questo modo, ma potrebbe essermi mancato qualche dettagli interni, non posso dire per certo)

Sono stato sorpreso di vedere una simile ottimizzazione, considerando quanto piccoli javac . Quanto dovremmo usare sempre final ? Non scriverò JMH un test JMH (che volevo inizialmente), sono sicuro che il diff è nell’ordine di ns (se ansible per essere catturato). L’unico posto in cui questo potrebbe essere un problema, è quando un metodo non può essere inline a causa della sua dimensione (e la dichiarazione final ridurrebbe quella dimensione di pochi byte).

Ci sono altre due final che devono essere affrontate. Il primo è quando un metodo è final (da una prospettiva JIT ), un tale metodo è monomorfico – e questi sono i più amati dalla JVM .

Poi ci sono le variabili di istanza final (che devono essere impostate in ogni costruttore); questi sono importanti in quanto garantiranno un riferimento correttamente pubblicato, come qui toccato un po ‘ e specificato esattamente dal JLS .

Nota: non è un esperto di java

Se ricordo correttamente il mio java, ci sarebbe ben poco per migliorare le prestazioni usando la parola chiave finale. Ho sempre saputo che esiste per “buon codice”: design e leggibilità.

Non sono un esperto, ma suppongo che dovresti aggiungere una parola chiave final alla class o al metodo se non verrà sovrascritto e lasciare le variabili da solo. Se ci sarà un modo per ottimizzare tali cose, il compilatore lo farà per te.

In realtà, durante il test di alcuni codici relativi a OpenGL, ho scoperto che l’utilizzo del modificatore finale su un campo privato può ridurre le prestazioni. Ecco l’inizio della class che ho testato:

 public class ShaderInput { private /* final */ float[] input; private /* final */ int[] strides; public ShaderInput() { this.input = new float[10]; this.strides = new int[] { 0, 4, 8 }; } public ShaderInput x(int stride, float val) { input[strides[stride] + 0] = val; return this; } // more stuff ... 

E questo è il metodo che ho usato per testare le prestazioni di varie alternative, tra cui la class ShaderInput:

 public static void test4() { int arraySize = 10; float[] fb = new float[arraySize]; for (int i = 0; i < arraySize; i++) { fb[i] = random.nextFloat(); } int times = 1000000000; for (int i = 0; i < 10; ++i) { floatVectorTest(times, fb); arrayCopyTest(times, fb); shaderInputTest(times, fb); directFloatArrayTest(times, fb); System.out.println(); System.gc(); } } 

Dopo la terza iterazione, con la VM riscaldata, ho costantemente ottenuto queste cifre senza la parola chiave finale:

 Simple array copy took : 02.64 System.arrayCopy took : 03.20 ShaderInput took : 00.77 Unsafe float array took : 05.47 

Con la parola chiave finale:

 Simple array copy took : 02.66 System.arrayCopy took : 03.20 ShaderInput took : 02.59 Unsafe float array took : 06.24 

Nota le figure per il test ShaderInput.

Non importa se ho reso i campi pubblici o privati.

Per inciso, ci sono alcune altre cose sconcertanti. La class ShaderInput supera tutte le altre varianti, anche con la parola chiave finale. Questo è notevole b / c fondamentalmente è una class che avvolge un array float, mentre gli altri test manipolano direttamente l'array. Devo capire questo fuori. Potrebbe avere qualcosa a che fare con l'interfaccia fluente di ShaderInput.

Anche System.arrayCopy sembra essere in qualche modo più lento per i piccoli array rispetto alla semplice copia di elementi da un array all'altro in un ciclo for. E usando sun.misc.Unsafe (così come java.nio.FloatBuffer diretto, non mostrato qui) si comporta in modo abissale.

Sicuramente sì, se la variabile si converte come costante,

come sappiamo, il compilatore java converte tali variabili finali come costanti quali possibili

poiché il concetto di compilatore java costante sostituisce direttamente il valore con il suo riferimento in fase di compilazione

nelle variabili java diventa costante se si tratta di una stringa o di un tipo primitivo che viene fornito senza alcun processo di runtime

altrimenti è solo una variabile finale (non variabile),

L’uso costante è sempre più veloce del riferimento.

quindi, se ansible, utilizzare le costanti in qualsiasi linguaggio di programmazione per prestazioni migliori

I membri dichiarati con finale saranno disponibili in tutto il programma perché, a differenza di quelli non definitivi, se questi membri non sono stati utilizzati nel programma, tuttavia non verranno presi in considerazione da Garbage Collector, quindi potrebbero causare problemi di prestazioni dovuti a una ctriggers gestione della memoria.

final parola chiave final può essere utilizzata in cinque modi in Java.

  1. Una class è definitiva
  2. Una variabile di riferimento è finale
  3. Una variabile locale è definitiva
  4. Un metodo è definitivo

Una class è definitiva: una class è finale significa che non possiamo essere estesi o che l’ereditarietà significa che l’ereditarietà non è ansible.

Allo stesso modo – Un object è definitivo: qualche volta non abbiamo modificato lo stato interno dell’object quindi in questo caso possiamo specificare che l’object è l’object finale. Oggetto finale significa non variabile anche finale.

Una volta che la variabile di riferimento è stata resa definitiva, non può essere riassegnata ad altri oggetti. Ma può cambiare il contenuto dell’object purché i suoi campi non siano definitivi