Разница между JVM LookupSwitch и TableSwitch?

Мне сложно понять LookUpSwitch и TableSwitch в байт-коде Java.

Если я хорошо понимаю, и LookUpSwitch и TableSwitch соответствуют оператору switch источника Java? Почему один оператор JAVA генерирует 2 разных байткода?

Документация Jasmin по каждому из них:

Ответ 1

Разница заключается в том, что lookupswitch использует таблицу с ключами и метками, но таблицаwitch использует таблицу только с метками.

При выполнении tablewitch значение int поверх стека непосредственно используется в качестве индекса в таблице, чтобы захватить пункт перехода и немедленно выполнить скачок. Весь процесс поиска + перехода - это операция O (1), что означает, что он быстро вспыхивает.

При выполнении lookupswitch значение int поверх стека сравнивается с клавишами в таблице до тех пор, пока не будет найдено совпадение, а затем пункт назначения перехода рядом с этим ключом используется для выполнения Прыгать. Поскольку таблица lookupswitch всегда должна сортироваться, так что keyX < keyY для каждого X < Y, весь процесс поиска + перехода является операцией O (log n), поскольку поиск ключа выполняется с использованием алгоритма бинарного поиска (нет необходимости сравнивать значение int со всеми возможными ключами, чтобы найти совпадение или определить, что ни один из ключей не соответствует). O (log n) несколько медленнее, чем O (1), но все же все в порядке, так как многие известные алгоритмы O (log n), и они обычно считаются быстрыми; даже O (n) или O (n * log n) по-прежнему считается довольно хорошим алгоритмом (медленные/плохие алгоритмы имеют O (n ^ 2), O (n ^ 3) или даже хуже).

Решение о том, какую инструкцию использовать компилятор, основывается на том, насколько компактен оператор switch, например

switch (inputValue) {
  case 1:  // ...
  case 2:  // ...
  case 3:  // ...
  default: // ...
}

Переключатель выше идеально компактен, он не имеет числовых "отверстий". Компилятор создаст такой стиль таблицы:

 tableswitch 1 3
    OneLabel
    TwoLabel
    ThreeLabel
  default: DefaultLabel

Псевдокод со страницы Jasmin объясняет это довольно хорошо:

int val = pop();                // pop an int from the stack
if (val < low || val > high) {  // if its less than <low> or greater than <high>,
    pc += default;              // branch to default 
} else {                        // otherwise
    pc += table[val - low];     // branch to entry in table
}

Этот код довольно четко описывает, как работает такой табличный интерфейс. val inputValue, low будет 1 (самое низкое значение в коммутаторе) и high будет 3 (самое высокое значение в коммутаторе).

Даже с некоторыми отверстиями переключатель может быть компактным, например

switch (inputValue) {
  case 1:  // ...
  case 3:  // ...
  case 4:  // ...
  case 5:  // ...
  default: // ...
}

Переключатель выше "почти компактный", он имеет только одно отверстие. Компилятор может сгенерировать следующую команду:

 tableswitch 1 6
    OneLabel
    FakeTwoLabel
    ThreeLabel
    FourLabel
    FiveLabel
  default: DefaultLabel

  ; <...code left out...>

  FakeTwoLabel:
  DefaultLabel:
    ; default code

Как вы можете видеть, компилятор должен добавить фальшивый футляр для 2, FakeTwoLabel. Поскольку 2 не является реальным значением коммутатора, FakeTwoLabel на самом деле является меткой, которая изменяет поток кода точно там, где находится случай по умолчанию, так как значение 2 должно фактически выполнять случай по умолчанию.

Таким образом, коммутатор не должен быть совершенно компактным для компилятора для создания столов-столов, но он должен быть, по крайней мере, достаточно близок к компактности. Теперь рассмотрим следующий переключатель:

switch (inputValue) {
  case 1:    // ...
  case 10:   // ...
  case 100:  // ...
  case 1000: // ...
  default:   // ...
}

Этот переключатель нигде не близок к компактности, он имеет более чем сто раз больше отверстий, чем значений. Можно было бы назвать это запасным переключателем. Компилятор должен был бы генерировать почти тысячи фальшивых случаев, чтобы выразить этот переключатель в виде таблицы. Результатом будет огромная таблица, резко увеличивающая размер файла класса. Это непрактично. Вместо этого он будет генерировать lookupswitch:

lookupswitch
    1       : Label1
    10      : Label10
    100     : Label100
    1000    : Label1000
    default : DefaultLabel

В этой таблице содержится всего 5 записей, а не более тысячи. Таблица имеет 4 действительных значения, O (log 4) равно 2 (log здесь находится на базе 2 BTW, а не на базе 10, поскольку компьютер работает с двоичными числами). Это означает, что VM не более двух сравнений, чтобы найти метку для inputValue или прийти к выводу, что значение не находится в таблице, и поэтому значение по умолчанию должно быть выполнено. Даже если в таблице было 100 записей, для получения правильной метки или принятия перехода на метку по умолчанию потребуется не более 7 сравнений VM (а 7 сравнений намного меньше 100 сравнений, не так ли?).

Итак, это глупость, что эти две инструкции взаимозаменяемы или что причина для двух инструкций имеет исторические причины. Существует две инструкции для двух разных ситуаций: одна для коммутаторов с компактными значениями (для максимальной скорости) и одна для переключателей с запасными значениями (не максимальная скорость, но все же хорошая скорость и очень компактное представление таблицы, независимо от всех числовых отверстий).

Ответ 2

Как javac 1.8.0_45 решает, к чему switch скомпилировать?

Для того, чтобы решить, когда использовать который можно использовать javac алгоритм выбора в качестве основы.

Мы знаем, что источник javac находится в langtools.

Тогда мы grep:

hg grep -i tableswitch

и первый результат это langtools/src/share/classes/com/sun/tools/javac/jvm/Gen.java:

// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
    nlabels > 0 &&
    table_space_cost + 3 * table_time_cost <=
    lookup_space_cost + 3 * lookup_time_cost
    ?
    tableswitch : lookupswitch;

Куда:

  • hi: максимальное значение регистра
  • lo: минимальное значение регистра

Таким образом, мы заключаем, что он учитывает как временную, так и пространственную сложность, с весом 3 для временной сложности.

TODO Я не понимаю, почему lookup_time_cost = nlabels а не log(nlabels), поскольку tableswitch может быть выполнено в O (log (n)) с помощью двоичного поиска.

Дополнительный факт: компиляторы C++ также делают аналогичный выбор между таблицей переходов O (1) и двоичным поиском O (long (n)): преимущество переключения на оператор if-else

Ответ 3

Спецификация виртуальной машины Java описывают разницу. "Команда tablewitch используется, когда случаи коммутатора могут быть эффективно представлены в виде индексов в таблицу смещений цели". Спецификация описывает более подробную информацию.

Ответ 4

Я подозреваю, что он в основном является историческим из-за некоторой специфической привязки байт-кода Java, чтобы подчеркнуть машинный код (например, собственный процессор Sun).

Столбец - это, по сути, вычисляемый прыжок, где пункт назначения берется из таблицы поиска. Напротив, lookupswitch требует сравнения каждого значения, в основном, элементов таблицы итераций, пока не будет найдено совпадающее значение.

Очевидно, что эти коды операций взаимозаменяемы, но на основе значений один или другой может быть более быстрым или более компактным (например, сравнить набор ключей с большими промежутками между ними и последовательным набором ключей).