¿Algún compilador para la JVM usa el goto "ancho"?

47

Me imagino que la mayoría de ustedes saben que gotoes una palabra clave reservada en el lenguaje Java pero que en realidad no se usa. Y probablemente también sepa que gotoes un código de operación de Java Virtual Machine (JVM). Creo que todas las estructuras de flujo de control sofisticados de Java, Scala y Kotlin son, a nivel JVM, implementado usando una combinación de gotoy ifeq, ifle, iflt, etc.

Mirando la especificación JVM https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w veo que también hay un goto_wcódigo de operación. Mientras que gototoma un desplazamiento de rama de 2 bytes, goto_wtoma un desplazamiento de rama de 4 bytes. La especificación establece que

Aunque la instrucción goto_w toma un desplazamiento de rama de 4 bytes, otros factores limitan el tamaño de un método a 65535 bytes (§4.11). Este límite puede aumentarse en una versión futura de Java Virtual Machine.

Me parece que goto_wes a prueba de futuro, como algunos de los otros *_wcódigos de operación. Pero también se me ocurre que tal vez goto_wpodría usarse con los dos bytes más significativos puestos a cero y los dos bytes menos significativos de la misma manera goto, con los ajustes necesarios.

Por ejemplo, dado este caso de conmutador de Java (o caso de coincidencia de Scala):

     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.

podríamos reescribirlo como

     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.

Realmente no he intentado esto, ya que probablemente he cometido un error al cambiar los "números de línea" para acomodar el goto_ws. Pero como está en la especificación, debería ser posible hacerlo.

Mi pregunta es si hay una razón por la que un compilador u otro generador de bytecode podría usar goto_wel límite actual de 65535 además de mostrar que se puede hacer.

Alonso del Arte
fuente

Respuestas:

51

El tamaño del código del método puede ser tan grande como 64K.

El desplazamiento de la ramificación del corto gotoes un entero de 16 bits con signo: de -32768 a 32767.

Entonces, el desplazamiento corto no es suficiente para hacer un salto desde el principio del método de 65K hasta el final.

Incluso a javacveces emite goto_w. Aquí hay un ejemplo:

public class WideGoto {

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

Descompilando 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
          ...
apangin
fuente
// ... repeat 10K times ...Eso compila? Sé que hay un límite para el tamaño de una sola clase de origen ... pero no sé cuál es precisamente (la generación de código es la única vez que he visto algo realmente golpearlo).
Elliott Frisch
3
@ElliottFrisch Lo hace. Siempre que el tamaño del
código
18
Frio. Gracias. Supongo que 64k deberían ser suficientes para cualquiera. ;)
Elliott Frisch
3
@ElliottFrisch: consejos sobre el sombrero como referencia.
TJ Crowder
34

No hay razón para usar goto_wcuando la rama encaja en a goto. Pero parece haber pasado por alto que las ramas son relativas , utilizando un desplazamiento firmado, ya que una rama también puede ir hacia atrás.

No lo nota cuando mira la salida de una herramienta como javap, ya que calcula la dirección de destino absoluta resultante antes de imprimir.

Por lo tanto goto, el rango de -327678 … +32767‬no siempre es suficiente para abordar cada posible ubicación objetivo en el 0 … +65535rango.

Por ejemplo, el siguiente método tendrá una goto_winstrucción al principio:

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 en Ideone

Compiled from "Main.java"
class LargeJump {
  public static void methodWithLargeJump(int);
    Code:
       0: iload_0
       1: ifeq          9
       4: goto_w        57567
…
Holger
fuente
77
Increible. Mi proyecto Java más grande, con algunos paquetes y unas pocas docenas de clases entre ellos, se compila a casi 200 KB. Pero tu Maincon methodWithLargeJump()compila casi 400KB.
Alonso del Arte
44
Eso demuestra cuánto Java está optimizado para el caso común ...
Holger
1
¿Cómo descubriste ese abuso de las tablas de salto? Código generado por la máquina?
Elliott Frisch
14
@ElliottFrisch Solo tuve que recordar que los finallybloques se duplican para un flujo normal y excepcional (obligatorio desde Java 6). Por lo tanto, anidar diez de ellos implica × 2¹⁰, entonces, el interruptor siempre tiene un objetivo predeterminado, por lo que junto con el iload, necesita diez bytes más relleno. También agregué una declaración no trivial en cada rama para evitar optimizaciones. La explotación de los límites es un tema recurrente, expresiones anidadas , lambdas , campos , constructores ...
Holger
2
Curiosamente, las expresiones anidadas y muchos constructores también alcanzan las limitaciones de implementación del compilador, no solo los límites del código de bytes. También hubo preguntas y respuestas sobre el tamaño máximo de archivo de clase (tal vez inconscientemente recordé la respuesta de Tagir al escribir esta respuesta). Finalmente , la longitud máxima del nombre del paquete y, en el lado JVM, la sincronización máxima anidada . Parece que la gente sigue siendo curiosa.
Holger
5

Parece que en algunos compiladores (probados en 1.6.0 y 11.0.7), si un método es lo suficientemente grande como para necesitar goto_w, usa exclusivamente goto_w. Incluso cuando tiene saltos muy locales, todavía usa goto_w.

David G.
fuente
1
¿Por qué podría ser eso? ¿Tiene algo que ver con el almacenamiento en caché de instrucciones?
Alexander - Restablece a Mónica el
@ Alexander-ReinstateMonica Probablemente solo sea fácil de implementar.
David G.