Algum compilador para a JVM usa o goto “wide”?

47

Eu acho que a maioria de vocês sabe que gotoé uma palavra-chave reservada na linguagem Java, mas na verdade não é usada. E você provavelmente também sabe que esse gotoé um código de operação da Java Virtual Machine (JVM). Eu conto todas as estruturas de controle de fluxo sofisticados de Java, Scala e Kotlin são, ao nível JVM, implementado usando uma combinação de gotoe ifeq, ifle, iflt, etc.

Observando as especificações da JVM https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w , vejo também que há um goto_wcódigo de operação. Considerando que gotoleva um deslocamento de ramificação de 2 bytes, goto_wleva um deslocamento de ramificação de 4 bytes. A especificação afirma que

Embora a instrução goto_w use um deslocamento de ramificação de 4 bytes, outros fatores limitam o tamanho de um método a 65535 bytes (§4.11). Esse limite pode ser aumentado em uma versão futura da Java Virtual Machine.

Parece-me goto_wà prova de futuro, como alguns dos outros *_wopcodes. Mas também me ocorre que talvez goto_wpossa ser usado com os dois bytes mais significativos zerados e os dois bytes menos significativos iguais, como gotocom os ajustes necessários.

Por exemplo, dado este Java Switch-Case (ou 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.

poderíamos reescrevê-lo 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.

Na verdade, eu não tentei isso, pois provavelmente cometi um erro ao alterar os "números de linha" para acomodar os goto_ws. Mas como está na especificação, deve ser possível fazê-lo.

Minha pergunta é se existe uma razão pela qual um compilador ou outro gerador de bytecode pode usar goto_wcom o atual limite 65535 diferente de mostrar que isso pode ser feito?

Alonso del Arte
fonte

Respostas:

51

O tamanho do código do método pode ser tão grande quanto 64K.

O deslocamento da ramificação do curto gotoé um número inteiro de 16 bits assinado: de -32768 a 32767.

Portanto, o deslocamento curto não é suficiente para dar um salto do início do método 65K para o final.

Mesmo javacàs vezes emite goto_w. Aqui está um exemplo:

public class WideGoto {

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

Descompilar com 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
fonte
// ... repeat 10K times ...Isso compila? Eu sei que há um limite para o tamanho de uma única classe de origem ... mas não sei exatamente o que é (a geração de código é a única vez que eu vejo algo realmente acontecer).
Elliott Frisch
3
@ElliottFrisch Sim. Contanto que o tamanho do bytecode do método não exceda 65535 e o comprimento constante do pool também seja menor que 65535.
apangin 18/04
18
Legal. Obrigado. 64k deve ser suficiente para quem eu acho. ;)
Elliott Frisch
3
@ ElliottFrisch - Chapéu de dicas na referência.
TJ Crowder
34

Não há razão para usar goto_wquando o ramo se encaixa em a goto. Mas você parece ter esquecido que as ramificações são relativas , usando um deslocamento assinado, pois uma ramificação também pode retroceder.

Você não percebe isso quando olha para a saída de uma ferramenta javap, pois calcula o endereço de destino absoluto resultante antes da impressão.

Portanto goto, o intervalo de -327678 … +32767‬nem sempre é suficiente para abordar cada local-alvo possível no 0 … +65535intervalo.

Por exemplo, o método a seguir terá uma goto_winstrução no início:

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() {}

Demonstração sobre Ideone

Compiled from "Main.java"
class LargeJump {
  public static void methodWithLargeJump(int);
    Code:
       0: iload_0
       1: ifeq          9
       4: goto_w        57567
…
Holger
fonte
7
Uau, incrível. Meu maior projeto Java, com alguns pacotes e algumas dezenas de classes entre eles, compila para quase 200 KB. Mas o seu Maincom methodWithLargeJump()compila para quase 400KB.
Alonso del Arte
4
Isso demonstra o quanto o Java é otimizado para o caso comum ...
Holger
11
Como você descobriu esse abuso de tabelas de salto? Código gerado por máquina?
Elliott Frisch
14
@ ElliottFrisch Eu só precisava lembrar que os finallyblocos são duplicados para um fluxo normal e excepcional (obrigatório desde o Java 6). Portanto, aninhar dez deles implica em × 2¹⁰; o comutador sempre tem um destino padrão; portanto, junto com o iload, ele precisa de dez bytes mais o preenchimento. Também adicionei uma declaração não trivial em cada ramo para evitar otimizações. Explorar limites é um tópico recorrente, expressões aninhadas , lambdas , campos , construtores
Holger
2
Curiosamente, expressões aninhadas e muitos construtores também atingem as limitações de implementação do compilador, não apenas os limites de bytecode. Também houve uma sessão de perguntas e respostas sobre o tamanho máximo do arquivo da turma (talvez eu tenha me lembrado inconscientemente da resposta de Tagir ao escrever essa resposta). Finalmente , o tamanho máximo do nome do pacote e, no lado da JVM, o máximo aninhado sincronizado . Parece que as pessoas continuam ficando curiosas.
Holger
5

Parece que em alguns compiladores (tentados em 1.6.0 e 11.0.7), se um método é grande o suficiente para a necessidade de goto_w, ele usa exclusivamente goto_w. Mesmo quando tem saltos muito locais, ainda usa goto_w.

David G.
fonte
11
Por que poderia ser isso? Tem algo a ver com o cache de instruções?
Alexander - Restabelecer Monica
@ Alexander-ReinstateMonica Provavelmente apenas facilidade de implementação.
David G.