Используют ли какие-либо компиляторы для JVM «широкий» переход?

47

Я думаю, что большинство из вас знает, что gotoэто зарезервированное ключевое слово в языке Java, но на самом деле оно не используется. И вы, вероятно, также знаете, что gotoэто код операции виртуальной машины Java (JVM). Я считаю , все сложные структуры потока управления Java, Scala и Котлин является, на уровне виртуальной машины Java, реализованы с использованием некоторой комбинации gotoи ifeq, ifle, ifltи т.д.

Глядя на спецификацию JVM https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w я вижу, что есть также goto_wкод операции. Принимая во внимание, что gotoпринимает 2-байтовое смещение ветви, goto_wпринимает 4-байтовое смещение ветви. В спецификации говорится, что

Хотя инструкция goto_w принимает 4-байтовое смещение ветви, другие факторы ограничивают размер метода 65535 байтами (§4.11). Этот предел может быть повышен в будущем выпуске виртуальной машины Java.

Для меня это звучит так, как будто goto_wоно ориентировано на будущее, как и некоторые другие *_wкоды операций. Но мне также кажется, что, возможно, goto_wможно было бы использовать обнуленные два более значимых байта и два менее значимых байта, такие же, как для goto, с корректировками по мере необходимости.

Например, учитывая этот Java Switch-Case (или 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.

мы могли бы переписать это как

     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.

Я на самом деле не пробовал это, так как я, вероятно, сделал ошибку, изменив «номера строк», чтобы приспособить goto_ws. Но так как это в спецификации, должно быть возможно сделать это.

Мой вопрос заключается в том, есть ли причина, по которой компилятор или другой генератор байт-кода может использовать goto_wс текущим пределом 65535, кроме того, чтобы показать, что это можно сделать?

Алонсо дель Арте
источник

Ответы:

51

Размер кода метода может достигать 64 КБ.

Смещение ветви короткого замыкания gotoпредставляет собой 16-разрядное целое число со знаком: от -32768 до 32767.

Таким образом, короткого смещения недостаточно для перехода от начала метода 65K к концу.

Даже javacиногда излучает goto_w. Вот пример:

public class WideGoto {

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

Декомпилирование с 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
источник
// ... repeat 10K times ...Что компилирует? Я знаю, что существует ограничение на размер одного исходного класса ... но я не знаю, что это такое (генерация кода - единственный раз, когда я видел, что что-то действительно достигло этого).
Эллиот Фриш
3
@ElliottFrisch Это делает. Пока размер байт-кода метода не превышает 65535, а постоянная длина пула также меньше 65535.
apangin
18
Круто. Спасибо. 64k должно быть достаточно для всех, я думаю. ;)
Эллиот Фриш
3
@ElliottFrisch - Подсказка шляпа при ссылке.
TJ Crowder
34

Нет смысла использовать, goto_wкогда ветвь вписывается в goto. Но вы, кажется, упустили, что ветви являются относительными , используя смещение со знаком, поскольку ветвь также может идти назад.

Вы не замечаете этого, глядя на выходные данные инструмента, подобного javap, поскольку он вычисляет результирующий абсолютный целевой адрес перед печатью.

Так gotoчто диапазона -327678 … +32767‬не всегда достаточно, чтобы охватить каждое возможное целевое местоположение в 0 … +65535диапазоне.

Например, следующий метод будет иметь goto_wинструкцию в начале:

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

Демо на Ideone

Compiled from "Main.java"
class LargeJump {
  public static void methodWithLargeJump(int);
    Code:
       0: iload_0
       1: ifeq          9
       4: goto_w        57567
…
Holger
источник
7
Ух ты, потрясающе. Мой самый большой Java-проект с несколькими пакетами и несколькими десятками классов между ними компилируется почти в 200 КБ. Но ваш Mainс methodWithLargeJump()компиляциями почти 400 КБ.
Алонсо дель Арте
4
Это показывает, насколько оптимизирована Java для общего случая ...
Хольгер,
1
Как вы обнаружили, что злоупотребление таблицами прыжков? Сгенерированный машиной код?
Эллиот Фриш
14
@ElliottFrisch Мне только нужно было вспомнить, что finallyблоки дублируются для нормального и исключительного потока (обязательно с Java 6). Таким образом, вложение десяти из них подразумевает × 2¹⁰, тогда у switch всегда есть цель по умолчанию, поэтому вместе с iload требуется десять байтов плюс заполнение. Я также добавил нетривиальный оператор в каждую ветку, чтобы предотвратить оптимизацию. Эксплуатация лимитов - это повторяющаяся тема, вложенные выражения , лямбды , поля , конструкторы ...
Хольгер,
2
Интересно, что вложенные выражения и множество конструкторов также затрагивают ограничения реализации компилятора, а не только ограничения байт-кода. Также был вопрос о максимальном размере файла класса (возможно, я подсознательно помнил ответ Тагира при написании этого ответа). Наконец, максимальная длина имени пакета и, на стороне JVM, максимальная вложенная синхронизация . Кажется, люди продолжают оставаться любопытными.
Хольгер
5

Похоже, что в некоторых компиляторах (пробовал в 1.6.0 и 11.0.7), если метод достаточно велик, когда-либо нужен goto_w, он использует исключительно goto_w. Даже когда у него очень локальные переходы, он все равно использует goto_w.

Дэвид Г.
источник
1
Почему это может быть? Это как-то связано с кэшированием инструкций?
Александр - Восстановить Монику
@ Alexander-ReinstateMonica Вероятно, просто простота реализации.
Дэвид Дж.