Perché Java ha un'istruzione bytecode IINC?
Perché Java ha un'istruzione bytecode IINC? Esiste già un'istruzione bytecode IADD. Quindi perché IINC esiste?
3 answers
Solo i progettisti originali di Java possono rispondere perché hanno preso particolari decisioni di progettazione. Tuttavia, possiamo speculare:
IINC non ti consente di fare nulla che non possa già essere realizzato da una combinazione ILOAD/SIPUSH/IADD/ISTORE. La differenza è che IINC è una singola istruzione, che richiede solo 3 o 6 byte, mentre la sequenza di istruzioni 4 è ovviamente più lunga. Quindi IINC riduce leggermente la dimensione del bytecode che lo utilizza.
Oltre a ciò, le prime versioni di Java utilizzavano un interperter, dove ogni istruzione ha un overhead durante l'esecuzione. In questo caso, l'utilizzo di una singola istruzione IINC potrebbe essere più veloce della sequenza di bytecode alternativa equivalente. Si noti che JITting ha reso questo in gran parte irrilevante, ma IINC risale alla versione originale di Java.
Guardando questa tabella , ci sono un paio di importanti differenze.
Iinc: incrementa la variabile locale # index per byte const firmato
-
iinc
utilizza un registro al posto dello stack. -
iinc
può aumentare solo di un valore di byte firmato. Se si desidera aggiungere[-128,127]
a un numero intero, è possibile utilizzareiinc
, ma non appena si desidera aggiungere un numero al di fuori di tale intervallo è necessario utilizzareisub
,iadd
, oppure istruzioni multipleiinc
.
E1:
TL;DR
Avevo fondamentalmente ragione, tranne che il limite è firmato valori brevi (16 bit [-32768,32767]
). C'è un'istruzione bytecode wide
che modifica iinc
(e un paio di altre istruzioni) per usare numeri a 16 bit invece di numeri a 8 bit.
Inoltre, considerare l'aggiunta di due variabili insieme. Se una delle variabili non è costante, il compilatore non sarà mai in grado di inline il suo valore in bytecode, quindi impossibile usare iinc
; dovrà usare iadd
.
package SO37056714;
public class IntegerIncrementTest {
public static void main(String[] args) {
int i = 1;
i += 5;
}
}
Ho intenzione di sperimentare con il pezzo di codice sopra. Così com'è, è usa iinc
, come previsto.
$ javap -c IntegerIncrementTest.class
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
public SO37056714.IntegerIncrementTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iinc 1, 5
5: return
}
i += 127
usa iinc
come previsto.
$ javap -c IntegerIncrementTest.class
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
public SO37056714.IntegerIncrementTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iinc 1, 127
5: return
}
i += 128
non usa più iinc
, ma invece iinc_w
:
$ javap -c IntegerIncrementTest.class
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
public SO37056714.IntegerIncrementTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iinc_w 1, 128
8: return
}
i -= 601
utilizza anche iinc_w
:
$ javap -c IntegerIncrementTest.class
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
public SO37056714.IntegerIncrementTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iinc_w 1, -601
8: return
}
Il suffisso _w
si riferisce al bytecode wide
, che consente costanti fino a 16 bit ([-32768, 32767]
).
Se proviamo i += 32768
, noi vedrà quello che ho predetto sopra:
$ javap -c IntegerIncrementTest.class
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
public SO37056714.IntegerIncrementTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iload_1
3: ldc #16 // int 32768
5: iadd
6: istore_1
7: return
}
Inoltre, considera il caso in cui stiamo aggiungendo un'altra variabile a i
(i += c
). Il compilatore non sa se c
è costante o meno, quindi non può inline il valore di c
in bytecode. Userà iadd
anche per questo caso:
int i = 1;
byte c = 3;
i += c;
$ javap -c IntegerIncrementTest.class
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
public SO37056714.IntegerIncrementTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_3
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_1
8: return
}
Come già sottolineato una singola istruzione iinc
è più breve della a iload
, sipush
, iadd
, istore
sequenza. Ci sono anche prove che l'esecuzione di una riduzione delle dimensioni del codice common-case è stata una motivazione importante.
Esistono istruzioni specializzate per gestire le prime quattro variabili locali, ad esempio aload_0
fa lo stesso di aload 0
e verrà usato spesso per caricare il riferimento this
sullo stack degli operando. C'è un'istruzione ldc
che può fare riferimento a una dei primi 255 elementi di pool costanti mentre tutti possono essere gestiti da ldc_w
, le istruzioni di branch usano due byte per gli offset, quindi solo metodi eccessivamente grandi devono ricorrere a goto_w
, e le istruzioni iconst_n
per -1
a 5
esistono nonostante tutti possano essere gestiti da bipush
che supporta valori che tutti potrebbero anche essere gestiti da sipush
, che potrebbero essere sostituiti da ldc
.
Quindi le istruzioni asimmetriche sono la norma. Nelle applicazioni tipiche, ci sono molti piccoli metodi con solo poche variabili locali e numeri più piccoli sono più comuni di numeri più grandi. iinc
è un equivalente diretto alle espressioni i++
o i+=smallConstantNumber
stand-alone (applicate alle variabili locali) che spesso si verificano all'interno dei loop. Essendo in grado di esprimere idiomi di codice comuni in codice più compatto senza perdere la possibilità di esprimere tutto il codice, otterrai grandi risparmi nella dimensione complessiva del codice.
Come anche già sottolineato, c'è solo una piccola opportunità per un'esecuzione più rapida in interpreted esecuzioni che sono irrilevanti per l'esecuzione di codice compilato/ottimizzato.