Czy jakieś kompilatory dla JVM używają „szerokiego” goto?

47

Myślę, że większość z was wie, że gotojest to zastrzeżone słowo kluczowe w języku Java, ale tak naprawdę nie jest używane. Prawdopodobnie wiesz również, że gotojest to kod operacyjny Java Virtual Machine (JVM). Sądzę wszystkich skomplikowanych struktur przepływu kontrola Java, Scala i Kotlin są na poziomie JVM, realizowane przy użyciu niektórych kombinacji gotoi ifeq, ifle, iflt, itd.

Patrząc na specyfikację JVM https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w Widzę, że istnieje również goto_wkod operacyjny. Podczas gdy gotoprzyjmuje 2-bajtowe przesunięcie gałęzi, goto_wprzyjmuje 4-bajtowe przesunięcie gałęzi. Specyfikacja mówi, że

Chociaż instrukcja goto_w przyjmuje 4-bajtowe przesunięcie gałęzi, inne czynniki ograniczają rozmiar metody do 65535 bajtów (§4.11). Limit ten może zostać podniesiony w przyszłej wersji wirtualnej maszyny Java.

To brzmi dla mnie jak goto_wjest przyszłość pokryć, jak niektóre inne *_wrozkazy. Ale zdaje mi się, że możegoto_w może można by go użyć z dwoma bardziej znaczącymi bajtami wyzerowanymi i dwoma mniej znaczącymi bajtami takimi samymi jak dla goto, z koniecznymi zmianami.

Na przykład, biorąc pod uwagę ten Java Switch-Case (lub 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.

moglibyśmy przepisać to jako

     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.

Tak naprawdę nie próbowałem tego, ponieważ prawdopodobnie popełniłem błąd, zmieniając „numery linii”, aby uwzględnić goto_ws. Ale ponieważ jest to w specyfikacji, powinno być to możliwe.

Moje pytanie brzmi, czy istnieje powód, dla którego kompilator lub inny generator kodu bajtowego mógłby użyć goto_wprzy obecnym limicie 65535, aby pokazać, że można to zrobić?

Alonso del Arte
źródło

Odpowiedzi:

51

Rozmiar kodu metody może wynosić nawet 64 KB.

Odgałęzienie zwarcia goto to 16-bitowa liczba całkowita ze znakiem: od -32768 do 32767.

Krótkie przesunięcie nie wystarczy, aby wykonać skok od początku metody 65K do końca.

Nawet javacczasami emituje goto_w. Oto przykład:

public class WideGoto {

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

Dekompilacja za pomocą 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
źródło
// ... repeat 10K times ...To się kompiluje? Wiem, że istnieje limit wielkości pojedynczej klasy źródłowej ... ale nie wiem, co to dokładnie jest (generowanie kodu to jedyny raz, kiedy widziałem, że coś w to uderzyło).
Elliott Frisch
3
@ElliottFrisch To robi. O ile rozmiar kodu bajtowego metody nie przekracza 65535, a stała długość puli jest również mniejsza niż 65535.
apangin
18
Fajne. Dzięki. 64k powinno wystarczyć każdemu, jak sądzę. ;)
Elliott Frisch
3
@ElliottFrisch - Porady kapelusz w referencji.
TJ Crowder
34

Nie ma powodu, aby używać, goto_wgdy gałąź pasuje do goto. Ale wydaje się, że przegapiłeś, że gałęzie są względne , używając przesunięcia z podpisem, ponieważ gałąź może również cofnąć się.

Nie zauważysz tego, patrząc na wynik takiego narzędzia javap, ponieważ oblicza wynikowy bezwzględny adres docelowy przed drukowaniem.

Tak więc gotozasięg -327678 … +32767‬nie zawsze wystarcza, aby zająć się każdą możliwą lokalizacją docelową w 0 … +65535zasięgu.

Na przykład następująca metoda będzie miała goto_winstrukcję na początku:

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

Compiled from "Main.java"
class LargeJump {
  public static void methodWithLargeJump(int);
    Code:
       0: iload_0
       1: ifeq          9
       4: goto_w        57567
…
Holger
źródło
7
Wow niesamowite. Mój największy projekt Java, z kilkoma pakietami i kilkadziesiąt klas między nimi, kompiluje się do prawie 200 KB. Ale twoja Mainz methodWithLargeJump()gromadzi do niemal 400KB.
Alonso del Arte
4
To pokazuje, ile Java jest zoptymalizowana dla typowego przypadku ...
Holger
1
Jak odkryłeś to nadużywanie skoków? Kod wygenerowany maszynowo?
Elliott Frisch
14
@ElliottFrisch Musiałem tylko przypomnieć, że finallybloki są duplikowane dla normalnego i wyjątkowego przepływu (obowiązkowe od Java 6). Zatem zagnieżdżenie dziesięciu z nich oznacza × 2¹⁰, wtedy przełącznik zawsze ma domyślny cel, więc wraz z iloadem potrzebuje dziesięciu bajtów plus dopełniania. Dodałem także nietrywialną instrukcję w każdej gałęzi, aby zapobiec optymalizacji. Wykorzystywanie limitów to powracający temat, zagnieżdżone wyrażenia , lambdy , pola , konstruktory
Holger
2
Co ciekawe, wyrażenia zagnieżdżone i wiele konstruktorów również uderza w ograniczenia implementacyjne kompilatora, nie tylko limity kodu bajtowego. Pojawiły się również pytania dotyczące maksymalnego rozmiaru pliku klasy (być może nieświadomie pamiętałem odpowiedź Tagira podczas pisania tej odpowiedzi). Wreszcie maksymalna długość nazwy pakietu, a po stronie JVM, maksymalna zagnieżdżona synchronizacja . Wygląda na to, że ludzie pozostają ciekawi.
Holger
5

Wygląda na to, że w niektórych kompilatorach (wypróbowanych w 1.6.0 i 11.0.7), jeśli metoda jest wystarczająco duża i zawsze potrzebuje goto_w, używa wyłącznie goto_w. Nawet jeśli ma bardzo lokalne skoki, nadal używa goto_w.

David G.
źródło
1
Dlaczego to może być? Czy ma to coś wspólnego z buforowaniem instrukcji?
Alexander - Przywróć Monikę
@ Alexander-ReinstateMonica Prawdopodobnie po prostu łatwość wdrożenia.
David G.