Alcuni compilatori per la JVM usano il goto "wide"?


47

Immagino che la maggior parte di voi sappia che gotoè una parola chiave riservata nel linguaggio Java ma non viene effettivamente utilizzata. E probabilmente saprai anche che gotoè un codice operativo JVM (Java Virtual Machine). Mi sa che tutte le sofisticate strutture di controllo del flusso di Java, Scala e Kotlin sono, a livello di JVM, implementato utilizzando una combinazione di gotoe ifeq, ifle, iflt, etc.

Osservando le specifiche JVM https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w vedo che c'è anche un goto_wcodice operativo. Considerando che gotoassume un offset di ramo a 2 byte, goto_waccetta un offset di ramo a 4 byte. Le specifiche lo affermano

Sebbene l' istruzione goto_w abbia un offset di ramo di 4 byte, altri fattori limitano la dimensione di un metodo a 65535 byte (§4.11). Questo limite potrebbe essere aumentato in una versione futura di Java Virtual Machine.

Mi sembra che goto_wsia a prova di futuro, come alcuni degli altri *_wcodici operativi. Ma mi viene anche in mente che forse goto_wpotrebbe essere usato con i due byte più significativi azzerati e i due byte meno significativi gli stessi di per goto, con le regolazioni necessarie.

Ad esempio, dato questo caso Switch Java (o Scala Match Case):

     12: lookupswitch  {
                112785: 48 // case "red"
               3027034: 76 // case "green"
              98619139: 62 // case "blue"
               default: 87
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          87
      57: iconst_0
      58: istore_3
      59: goto          87
      62: aload_2
      63: ldc           #19                 // String green
      65: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      68: ifeq          87
      71: iconst_1
      72: istore_3
      73: goto          87
      76: aload_2
      77: ldc           #20                 // String blue
      79: invokevirtual #18 
      // etc.

potremmo riscriverlo come

     12: lookupswitch  { 
                112785: 48
               3027034: 78
              98619139: 64
               default: 91
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          91 // 00 5B
      57: iconst_0
      58: istore_3
      59: goto_w        91 // 00 00 00 5B
      64: aload_2
      65: ldc           #19                 // String green
      67: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      70: ifeq          91
      73: iconst_1
      74: istore_3
      75: goto_w          91
      79: aload_2
      81: ldc           #20                 // String blue
      83: invokevirtual #18 
      // etc.

In realtà non ho provato questo, dal momento che probabilmente ho fatto un errore cambiando i "numeri di riga" per adattarsi alla goto_ws. Ma poiché è nelle specifiche, dovrebbe essere possibile farlo.

La mia domanda è se c'è un motivo che un compilatore o un altro generatore di bytecode potrebbe usare goto_wcon l'attuale limite 65535 oltre a dimostrare che può essere fatto?

Risposte:


51

La dimensione del codice del metodo può essere grande quanto 64 KB.

L'offset del ramo del corto gotoè un numero intero a 16 bit con segno: da -32768 a 32767.

Pertanto, l'offset breve non è sufficiente per fare un salto dall'inizio del metodo 65K alla fine.

Anche a javacvolte emette goto_w. Ecco un esempio:

public class WideGoto {

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000_000; ) {
            i += 123456;
            // ... repeat 10K times ...
        }
    }
}

Decompilazione con javap -c:

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: ldc           #2
       5: if_icmplt     13
       8: goto_w        50018     // <<< Here it is! A jump to the end of the loop
          ...

// ... repeat 10K times ...Quello si compila? So che esiste un limite alla dimensione di una singola classe di origine ... ma non so cosa sia esattamente (la generazione del codice è l'unica volta che ho visto qualcosa che l'ha effettivamente colpito).
Elliott Frisch,

3
@ElliottFrisch Lo fa. Fintanto che la dimensione del bytecode del metodo non supera 65535 e anche la lunghezza costante del pool è inferiore a 65535.
apangin

18
Freddo. Grazie. 64k dovrebbe essere sufficiente per chiunque immagino. ;)
Elliott Frisch il

3
@ElliottFrisch - Cappello suggerimenti a riferimento.
TJ Crowder,

34

Non c'è motivo di usare goto_wquando il ramo si inserisce in a goto. Ma sembra che tu abbia perso che i rami sono relativi , usando un offset con segno, poiché un ramo può anche tornare indietro.

Non lo noti quando guardi l'output di uno strumento come javap, poiché calcola l'indirizzo di destinazione assoluto risultante prima della stampa.

Pertanto goto, l'intervallo di -327678 … +32767‬non è sempre sufficiente per indirizzare ogni possibile posizione di destinazione 0 … +65535nell'intervallo.

Ad esempio, il seguente metodo avrà goto_wun'istruzione all'inizio:

public static void methodWithLargeJump(int i) {
    for(; i == 0;) {
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
        } } } } } } } } } } } } } } } } } } } } 
    }
}
static void x() {}

Demo su Ideone

Compiled from "Main.java"
class LargeJump {
  public static void methodWithLargeJump(int);
    Code:
       0: iload_0
       1: ifeq          9
       4: goto_w        57567
…

7
Wow fantastico. Il mio più grande progetto Java, con alcuni pacchetti e alcune decine di classi tra loro, si compila a quasi 200 KB. Ma il tuo Maincon methodWithLargeJump()compilazioni fino a quasi 400 KB.
Alonso del Arte,

4
Ciò dimostra quanto Java sia ottimizzato per il caso comune ...
Holger,

1
Come hai scoperto quell'abuso dei jump table? Codice generato dalla macchina?
Elliott Frisch,

14
@ElliottFrisch Ho solo dovuto ricordare che i finallyblocchi vengono duplicati per un flusso normale ed eccezionale (obbligatorio da Java 6). Quindi nidificarne dieci implica × 2¹⁰, quindi switch ha sempre un target predefinito, quindi insieme a iload, ha bisogno di dieci byte più il riempimento. Ho anche aggiunto un'istruzione non banale in ciascun ramo per evitare ottimizzazioni. Lo sfruttamento dei limiti è un argomento ricorrente, espressioni nidificate , lambda , campi , costruttori ...
Holger,

2
È interessante notare che espressioni nidificate e molti costruttori colpiscono anche i limiti dell'implementazione del compilatore, non solo i limiti di bytecode. C'è stato anche un D&R sulla dimensione massima del file di classe (forse ho inconsciamente ricordato la risposta di Tagir quando ho scritto questa risposta). Finalmente la lunghezza massima del nome del pacchetto e, sul lato JVM, il massimo nidificato sincronizzato . Sembra che la gente rimanga curiosa.
Holger,

5

Sembra che in alcuni compilatori (provato in 1.6.0 e 11.0.7), se un metodo è abbastanza grande, ha sempre bisogno di goto_w, usa esclusivamente goto_w. Anche quando ha salti molto locali, usa ancora goto_w.


1
Perché potrebbe essere? Ha a che fare con la memorizzazione nella cache delle istruzioni?
Alexander - Ripristina Monica il

@ Alexander-ReinstateMonica Probabilmente solo facilità di implementazione.
David G.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.