Принудительный столовый переключатель вместо переключателя lookupswitch

Scala 2.11 компилирует выражение match в относительно плотном диапазоне Int в lookupswitch:

lookupswitch { // 21
    -12: 200
    -11: 200
    -10: 184
     -9: 190
     -8: 190
     -7: 190
     -6: 190
     -5: 190
     -4: 200
     -1: 200
      2: 195
      3: 195
      4: 195
      5: 195
      6: 184
      7: 184
     12: 184
     13: 184
     18: 184
     21: 184
     25: 184
default: 180
}

В то время как Java 7 компилирует эквивалентный оператор switch в tableswitch:

tableswitch { // -12 to 25
    -12: 168
    -11: 168
    -10: 177
     -9: 174
     -8: 174
     -7: 174
     -6: 174
     -5: 174
     -4: 168
     -3: 185
     -2: 185
     -1: 168
      0: 185
      1: 185
      2: 171
      3: 171
      4: 171
      5: 171
      6: 177
      7: 177
      8: 185
      9: 185
     10: 185
     11: 185
     12: 181
     13: 181
     14: 185
     15: 185
     16: 185
     17: 185
     18: 181
     19: 185
     20: 185
     21: 181
     22: 185
     23: 185
     24: 185
     25: 181
default: 185
}

Есть ли способ заставить Scala генерировать a tableswitch?

Ответ 1

Вы не должны заботиться о байт-коде, поскольку современные JVM достаточно умны, чтобы скомпилировать как lookupswitch, так и tableswitch таким же эффективным способом.

Интуитивно tableswitch должен быть быстрее, и это также предлагается Спецификация JVM:

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

Однако спецификация была написана еще 20 лет назад, когда JVM не имела компилятора JIT. Есть ли разница в производительности в современной JVM HotSpot?

Тест

package bench;

import org.openjdk.jmh.annotations.*;

@State(Scope.Benchmark)
public class SwitchBench {
    @Param({"1", "2", "3", "4", "5", "6", "7", "8"})
    int n;

    @Benchmark
    public long lookupSwitch() {
        return Switch.lookupSwitch(n);
    }

    @Benchmark
    public long tableSwitch() {
        return Switch.tableSwitch(n);
    }
}

Чтобы иметь точный контроль над байт-кодом, я строю класс Switch с Jasmin.

.class public bench/Switch
.super java/lang/Object

.method public static lookupSwitch(I)I
    .limit stack 1

    iload_0
    lookupswitch
      1 : One
      2 : Two
      3 : Three
      4 : Four
      5 : Five
      6 : Six
      7 : Seven
      default : Other

One:
    bipush 11
    ireturn
Two:
    bipush 22
    ireturn
Three:
    bipush 33
    ireturn
Four:
    bipush 44
    ireturn
Five:
    bipush 55
    ireturn
Six:
    bipush 66
    ireturn
Seven:
    bipush 77
    ireturn
Other:
    bipush -1
    ireturn
.end method

.method public static tableSwitch(I)I
    .limit stack 1

    iload_0
    tableswitch 1
      One
      Two
      Three
      Four
      Five
      Six
      Seven
      default : Other

One: 
    bipush 11
    ireturn
Two:
    bipush 22
    ireturn
Three:
    bipush 33
    ireturn
Four:
    bipush 44
    ireturn
Five:
    bipush 55
    ireturn
Six:
    bipush 66
    ireturn
Seven:
    bipush 77
    ireturn
Other:
    bipush -1
    ireturn
.end method

Результаты не показывают разницы в производительности между показателями lookupswitch/tableswitch, но есть небольшое изменение в зависимости от аргумента switch:

lookupswitch vs. performancewitch performance

Сборка

Проверим теорию, посмотрев на сгенерированный ассемблерный код.
Следующая опция JVM поможет: -XX:CompileCommand=print,bench.Switch::*

  # {method} {0x0000000017498a48} 'lookupSwitch' '(I)I' in 'bench/Switch'
  # parm0:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x000000000329b240: sub    $0x18,%rsp
  0x000000000329b247: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - bench.Switch::[email protected]

  0x000000000329b24c: cmp    $0x4,%edx
  0x000000000329b24f: je     0x000000000329b2a5
  0x000000000329b251: cmp    $0x4,%edx
  0x000000000329b254: jg     0x000000000329b281
  0x000000000329b256: cmp    $0x2,%edx
  0x000000000329b259: je     0x000000000329b27a
  0x000000000329b25b: cmp    $0x2,%edx
  0x000000000329b25e: jg     0x000000000329b273
  0x000000000329b260: cmp    $0x1,%edx
  0x000000000329b263: jne    0x000000000329b26c  ;*lookupswitch
                                                 ; - bench.Switch::[email protected]
  ...

Здесь мы видим двоичный поиск, начинающийся со среднего значения 4 (это объясняет, почему случай 4 имеет лучшую производительность на графике выше).

Но интересно то, что tableswitch скомпилируется точно так же!

  # {method} {0x0000000017528b18} 'tableSwitch' '(I)I' in 'bench/Switch'
  # parm0:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x000000000332c280: sub    $0x18,%rsp
  0x000000000332c287: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - bench.Switch::[email protected]

  0x000000000332c28c: cmp    $0x4,%edx
  0x000000000332c28f: je     0x000000000332c2e5
  0x000000000332c291: cmp    $0x4,%edx
  0x000000000332c294: jg     0x000000000332c2c1
  0x000000000332c296: cmp    $0x2,%edx
  0x000000000332c299: je     0x000000000332c2ba
  0x000000000332c29b: cmp    $0x2,%edx
  0x000000000332c29e: jg     0x000000000332c2b3
  0x000000000332c2a0: cmp    $0x1,%edx
  0x000000000332c2a3: jne    0x000000000332c2ac  ;*tableswitch
                                                 ; - bench.Switch::[email protected]
  ...

Таблица перехода

Но подождите... Почему двоичный поиск, а не таблица перехода?

JVM HotSpot имеет эвристику для генерации таблицы перехода только для коммутаторов с 10+ случаями. Это может быть изменено опцией -XX:MinJumpTableSize=.

ОК, позвольте расширить наш тестовый пример еще несколькими ярлыками и снова проверить сгенерированный код.

  # {method} {0x0000000017288a68} 'lookupSwitch' '(I)I' in 'bench/Switch'
  # parm0:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x000000000307ecc0: sub    $0x18,%rsp         ;   {no_reloc}
  0x000000000307ecc7: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - bench.Switch::[email protected]

  0x000000000307eccc: mov    %edx,%r10d
  0x000000000307eccf: dec    %r10d
  0x000000000307ecd2: cmp    $0xa,%r10d
  0x000000000307ecd6: jb     0x000000000307ece9
  0x000000000307ecd8: mov    $0xffffffff,%eax
  0x000000000307ecdd: add    $0x10,%rsp
  0x000000000307ece1: pop    %rbp
  0x000000000307ece2: test   %eax,-0x1faece8(%rip)        # 0x00000000010d0000
                                                ;   {poll_return}
  0x000000000307ece8: retq   
  0x000000000307ece9: movslq %edx,%r10
  0x000000000307ecec: movabs $0x307ec60,%r11    ;   {section_word}
  0x000000000307ecf6: jmpq   *-0x8(%r11,%r10,8)  ;*lookupswitch
                                                ; - bench.Switch::[email protected]
                      ^^^^^^^^^^^^^^^^^^^^^^^^^

Да! Вот наша вычисленная команда перехода. Обратите внимание, что это генерируется для lookupswitch. Но для tableswitch будет точно такой же.

Удивительно, что JSM HotSpot может генерировать таблицы перехода даже для переключателей с разрывами и с выбросами. -XX:MaxJumpTableSparseness определяет, насколько велики пробелы. Например. если есть метки от 1 до 10, то от 13 до 20, а последняя метка со значением 99 - JIT будет генерировать проверочный тест для значения 99, а для остальных меток - создать таблицу.

Исходный код

Исходный код HotSpot, наконец, убедится, что не должно быть разницы в производительности между lookupswitch и tableswitch после того, как метод JIT-скомпилирован с C2. Это в основном потому, что синтаксический анализ обеих команд заканчивается вызовом той же функции jump_switch_ranges, которая работает для произвольного набора меток.

Заключение

Как мы видели, JVM HotSpot может скомпилировать tableswitch с помощью двоичного поиска и lookupswitch с помощью таблицы переходов или наоборот. Это зависит от количества и плотности меток, но не от самого байткода.

Итак, отвечая на ваш оригинальный вопрос - , вам не нужно!

Ответ 2

EDIT: вы можете проверить создание таблицы ИЛИ lookupswitch, аннотируя соответствие так:

import scala.annotation.switch

(foo: @switch) match {
  case 0 =>
  case 1 =>
  //And so forth
}

Если заданное совпадение невозможно скомпилировать в таблицу ИЛИ lookupswitch, это заставит его регистрировать предупреждение во время компиляции.