Чому, як виявляється, перемикання Java на суміжні вставки працює швидше із доданими справами?


276

Я працюю над деяким кодом Java, який потрібно оптимізувати, оскільки він буде працювати в гарячих функціях, які викликаються в багатьох точках моєї основної логіки програми. Частина цього коду включає множення doubleзмінних на 10підняті до довільних негативних int exponents. Одним із швидких способів (відредагувати: але не найшвидше, див. Оновлення 2 нижче) отримати помножене значення switchна exponent:

double multiplyByPowerOfTen(final double d, final int exponent) {
   switch (exponent) {
      case 0:
         return d;
      case 1:
         return d*10;
      case 2:
         return d*100;
      // ... same pattern
      case 9:
         return d*1000000000;
      case 10:
         return d*10000000000L;
      // ... same pattern with long literals
      case 18:
         return d*1000000000000000000L;
      default:
         throw new ParseException("Unhandled power of ten " + power, 0);
   }
}

Коментовані еліпси вище вказують на те, що case intконстанти продовжують збільшуватися на 1, тому caseу наведеному фрагменті коду дійсно є 19 с. Так як я не був упевнений , чи буду я на справді потрібна все сили 10 в caseзвітності 10через 18, я провів кілька microbenchmarks порівнюють час для завершення 10 мільйонів операцій з цим switchтвердженням проти switchтільки з caseS 0через 9exponentобмеженим до 9 або менше уникати порушеного впорядкування switch). Я отримав досить дивний (мені щонайменше!) Результат, що чим довше, switchтим більше caseзаяв насправді бігав швидше.

На жайворонку я спробував додати ще більше cases, що щойно повернув фіктивні значення, і виявив, що я можу змусити перемикач працювати ще швидше з приблизно 22-27 оголошеними cases (хоча ці фіктивні випадки ніколи фактично не потрапляють під час запуску коду ). (Знову ж таки, caseдодано s безперервно, збільшуючи попередню caseконстанту на 1.) Ці різниці у часі виконання не дуже істотні: для випадкового exponentміж 0та 10, switchпідкладеним манекеном виписки закінчується 10 мільйонів виконань за 1,49 сек проти 1,54 секунди для нерозкладених версія, для загальної економії 5ns за виконання. Отже, не та річ, яка змушує нав'язливо ставитись до прокладкиswitchзаява, яка вартує зусиль з точки зору оптимізації. Але мені все одно просто цікаво і контр-інтуїтивно зрозуміло, що switchне стає повільнішим (або, в кращому випадку, підтримувати постійний час (1) ) для виконання, оскільки caseдо нього додається більше s.

переключити результати бенчмаркінгу

Це результати, які я отримав від запуску з різними обмеженнями на випадково генеровані exponentзначення. Я не включають результати все аж до 1на exponentмежі, але загальна форма кривої залишається тим же, з хребта навколо 12-17 кейсів і долині між 18-28. Усі тести виконувались у JUnitBenchmark, використовуючи спільні контейнери для випадкових значень, щоб забезпечити однакові входи тестування. Я також проводив тести як для того, щоб від найдовшого switchтвердження до найкоротшого, так і навпаки, щоб спробувати усунути можливість тестових проблем, пов’язаних із замовленням. Я поставив свій тестовий код на репортаж github, якщо хтось хоче спробувати відтворити ці результати.

Отже, що тут відбувається? Якісь капризи моєї архітектури чи мікро-орієнтири? Або це Java switchдійсно трохи швидше , щоб виконати в 18в 28 caseдіапазоні , ніж від 11до 17?

github test repo "перемикач-експеримент"

ОНОВЛЕННЯ: Я трохи очистив бібліотеку бенчмаркінгу і додав текстовий файл в / результати з деяким результатом у ширшому діапазоні можливих exponentзначень. Я також додав у тестовому коді опцію не кидати Exceptionз default, але це, здається, не впливає на результати.

ОНОВЛЕННЯ 2: Знайдено досить вдале обговорення цього питання ще в 2009 році на форумі xkcd тут: http://forums.xkcd.com/viewtopic.php?f=11&t=33524 . Обговорення ОП щодо використання Array.binarySearch()дало мені ідею для простої реалізації на основі масиву схеми експоненції, наведеної вище. У двійковому пошуку немає потреби, оскільки я знаю, що це за записи array. Здається, він працює приблизно в 3 рази швидше, ніж використання switch, очевидно, за рахунок деякого потоку управління, який switchнадає. Цей код також додано до github repo.


64
Зараз у всіх службовців Google всюди буде розміщено точно 22 випадки у всіх switchзаявах, оскільки це, безумовно, найбільш оптимальне рішення. : D (Не
показуй

2
У вас є простіший SSCCE? Цей не компілюється для мене. Настільки ж слабкий, як я, на продуктивність Java, я хочу сфотографуватися на цьому.
Містичний

5
У моїй відповіді про рядкові випадки корисним може бути розділ "Перемикачі в JVM" . Я думаю, що тут відбувається те, що ви переходите з " lookupswitchна" tableswitch. Розбирання коду на javapвас покаже точно.
erickson

2
Я додав банки залежностей у папку / lib в репо. @Mysticial Вибачте, я свого роду вже витратив занадто багато часу, спускаючись з цієї кролячої нори! Якщо ви знімаєте "розширює AbstractBenchmark" з тестових класів і позбавляєтесь від імпорту "com.carrotsearch", ви можете запустити лише залежність від JUnit, але матеріал з морквою досить приємний для фільтрації частини шуму з JIT та періоди розминки. На жаль, я не знаю, як запускати ці тести JUnit поза IntelliJ.
Ендрю Бісселл

2
@AndrewBissell мені вдалося спростувати ваші результати набагато простішим орієнтиром. Дещо очевидна здогадка була галузевою та табличною галуззю для малих та середніх показників. Але я не маю кращого розуміння, ніж будь-хто інший, про занурення, яке відбувається в 30 випадках ...
Mysticial

Відповіді:


228

Як вказує інша відповідь , оскільки значення регістрів є суміжними (на відміну від розріджених), генерований байт-код для різних тестів використовує таблицю комутації (інструкція щодо байт-коду tableswitch).

Однак, як тільки JIT починає свою роботу і збирає байтовий код у збірку, tableswitch інструкція не завжди призводить до масиву покажчиків: іноді таблиця комутаторів перетворюється на те, що схоже на lookupswitch(схоже на if/ else ifструктуру).

Декомпіляція збірки, згенерованої JIT (точка доступу JDK 1.7), показує, що вона використовує послідовність if / else, якщо в 17 випадках і менше, масив покажчиків, коли їх більше 18 (більш ефективний).

Причина, по якій використовується це магічне число 18, здається, знижується до значення за замовчуванням MinJumpTableSize прапора JVM (близько рядка 352 у коді).

Я порушив цю проблему в списку компіляторів гарячої точки, і це здається спадщиною минулого тестування . Зауважте, що це значення за замовчуванням було видалено в JDK 8 після було проведено більше тестування .

Нарешті, коли метод стає занадто довгим (> 25 випадків у моїх тестах), він більше не вказується в налаштуваннях JVM за замовчуванням - це найвірогідніша причина зниження продуктивності в цій точці.


У 5 випадках декомпільований код виглядає приблизно так (зверніть увагу на інструкції cmp / je / jg / jmp, збірку для if / goto):

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x00000000024f0160: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x00000000024f0167: push   rbp
  0x00000000024f0168: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x00000000024f016c: cmp    edx,0x3
  0x00000000024f016f: je     0x00000000024f01c3
  0x00000000024f0171: cmp    edx,0x3
  0x00000000024f0174: jg     0x00000000024f01a5
  0x00000000024f0176: cmp    edx,0x1
  0x00000000024f0179: je     0x00000000024f019b
  0x00000000024f017b: cmp    edx,0x1
  0x00000000024f017e: jg     0x00000000024f0191
  0x00000000024f0180: test   edx,edx
  0x00000000024f0182: je     0x00000000024f01cb
  0x00000000024f0184: mov    ebp,edx
  0x00000000024f0186: mov    edx,0x17
  0x00000000024f018b: call   0x00000000024c90a0  ; OopMap{off=48}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
                                                ;   {runtime_call}
  0x00000000024f0190: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
  0x00000000024f0191: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffffa7]        # 0x00000000024f0140
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@52 (line 62)
                                                ;   {section_word}
  0x00000000024f0199: jmp    0x00000000024f01cb
  0x00000000024f019b: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff8d]        # 0x00000000024f0130
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@46 (line 60)
                                                ;   {section_word}
  0x00000000024f01a3: jmp    0x00000000024f01cb
  0x00000000024f01a5: cmp    edx,0x5
  0x00000000024f01a8: je     0x00000000024f01b9
  0x00000000024f01aa: cmp    edx,0x5
  0x00000000024f01ad: jg     0x00000000024f0184  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x00000000024f01af: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff81]        # 0x00000000024f0138
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@64 (line 66)
                                                ;   {section_word}
  0x00000000024f01b7: jmp    0x00000000024f01cb
  0x00000000024f01b9: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff67]        # 0x00000000024f0128
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@70 (line 68)
                                                ;   {section_word}
  0x00000000024f01c1: jmp    0x00000000024f01cb
  0x00000000024f01c3: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff55]        # 0x00000000024f0120
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x00000000024f01cb: add    rsp,0x10
  0x00000000024f01cf: pop    rbp
  0x00000000024f01d0: test   DWORD PTR [rip+0xfffffffffdf3fe2a],eax        # 0x0000000000430000
                                                ;   {poll_return}
  0x00000000024f01d6: ret    

З 18 випадків збірка виглядає приблизно так (зауважте масив покажчиків, який використовується, і пригнічує необхідність у всіх порівняннях: jmp QWORD PTR [r8+r10*1]стрибає прямо в потрібне множення) - це ймовірна причина для покращення продуктивності:

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x000000000287fe20: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x000000000287fe27: push   rbp
  0x000000000287fe28: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000287fe2c: cmp    edx,0x13
  0x000000000287fe2f: jae    0x000000000287fe46
  0x000000000287fe31: movsxd r10,edx
  0x000000000287fe34: shl    r10,0x3
  0x000000000287fe38: movabs r8,0x287fd70       ;   {section_word}
  0x000000000287fe42: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x000000000287fe46: mov    ebp,edx
  0x000000000287fe48: mov    edx,0x31
  0x000000000287fe4d: xchg   ax,ax
  0x000000000287fe4f: call   0x00000000028590a0  ; OopMap{off=52}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
                                                ;   {runtime_call}
  0x000000000287fe54: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
  0x000000000287fe55: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe8b]        # 0x000000000287fce8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@194 (line 92)
                                                ;   {section_word}
  0x000000000287fe5d: jmp    0x000000000287ff16
  0x000000000287fe62: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe86]        # 0x000000000287fcf0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@188 (line 90)
                                                ;   {section_word}
  0x000000000287fe6a: jmp    0x000000000287ff16
  0x000000000287fe6f: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe81]        # 0x000000000287fcf8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@182 (line 88)
                                                ;   {section_word}
  0x000000000287fe77: jmp    0x000000000287ff16
  0x000000000287fe7c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe7c]        # 0x000000000287fd00
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@176 (line 86)
                                                ;   {section_word}
  0x000000000287fe84: jmp    0x000000000287ff16
  0x000000000287fe89: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe77]        # 0x000000000287fd08
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@170 (line 84)
                                                ;   {section_word}
  0x000000000287fe91: jmp    0x000000000287ff16
  0x000000000287fe96: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe72]        # 0x000000000287fd10
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@164 (line 82)
                                                ;   {section_word}
  0x000000000287fe9e: jmp    0x000000000287ff16
  0x000000000287fea0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe70]        # 0x000000000287fd18
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@158 (line 80)
                                                ;   {section_word}
  0x000000000287fea8: jmp    0x000000000287ff16
  0x000000000287feaa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6e]        # 0x000000000287fd20
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@152 (line 78)
                                                ;   {section_word}
  0x000000000287feb2: jmp    0x000000000287ff16
  0x000000000287feb4: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe24]        # 0x000000000287fce0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@146 (line 76)
                                                ;   {section_word}
  0x000000000287febc: jmp    0x000000000287ff16
  0x000000000287febe: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6a]        # 0x000000000287fd30
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@140 (line 74)
                                                ;   {section_word}
  0x000000000287fec6: jmp    0x000000000287ff16
  0x000000000287fec8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe68]        # 0x000000000287fd38
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@134 (line 72)
                                                ;   {section_word}
  0x000000000287fed0: jmp    0x000000000287ff16
  0x000000000287fed2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe66]        # 0x000000000287fd40
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@128 (line 70)
                                                ;   {section_word}
  0x000000000287feda: jmp    0x000000000287ff16
  0x000000000287fedc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe64]        # 0x000000000287fd48
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@122 (line 68)
                                                ;   {section_word}
  0x000000000287fee4: jmp    0x000000000287ff16
  0x000000000287fee6: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe62]        # 0x000000000287fd50
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@116 (line 66)
                                                ;   {section_word}
  0x000000000287feee: jmp    0x000000000287ff16
  0x000000000287fef0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe60]        # 0x000000000287fd58
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@110 (line 64)
                                                ;   {section_word}
  0x000000000287fef8: jmp    0x000000000287ff16
  0x000000000287fefa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5e]        # 0x000000000287fd60
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@104 (line 62)
                                                ;   {section_word}
  0x000000000287ff02: jmp    0x000000000287ff16
  0x000000000287ff04: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5c]        # 0x000000000287fd68
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@98 (line 60)
                                                ;   {section_word}
  0x000000000287ff0c: jmp    0x000000000287ff16
  0x000000000287ff0e: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe12]        # 0x000000000287fd28
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x000000000287ff16: add    rsp,0x10
  0x000000000287ff1a: pop    rbp
  0x000000000287ff1b: test   DWORD PTR [rip+0xfffffffffd9b00df],eax        # 0x0000000000230000
                                                ;   {poll_return}
  0x000000000287ff21: ret    

І нарешті збірка з 30 випадків (нижче) схожа на 18 випадків, за винятком додаткового, movapd xmm0,xmm1який з’являється у середині коду, як помітив @cHao - однак найвірогіднішою причиною падіння продуктивності є те, що метод занадто довго бути накресленим за допомогою стандартних параметрів JVM:

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x0000000002524560: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x0000000002524567: push   rbp
  0x0000000002524568: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000252456c: movapd xmm1,xmm0
  0x0000000002524570: cmp    edx,0x1f
  0x0000000002524573: jae    0x0000000002524592  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524575: movsxd r10,edx
  0x0000000002524578: shl    r10,0x3
  0x000000000252457c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe3c]        # 0x00000000025243c0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@364 (line 118)
                                                ;   {section_word}
  0x0000000002524584: movabs r8,0x2524450       ;   {section_word}
  0x000000000252458e: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524592: mov    ebp,edx
  0x0000000002524594: mov    edx,0x31
  0x0000000002524599: xchg   ax,ax
  0x000000000252459b: call   0x00000000024f90a0  ; OopMap{off=64}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
                                                ;   {runtime_call}
  0x00000000025245a0: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
  0x00000000025245a1: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe27]        # 0x00000000025243d0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@358 (line 116)
                                                ;   {section_word}
  0x00000000025245a9: jmp    0x0000000002524744
  0x00000000025245ae: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe22]        # 0x00000000025243d8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@348 (line 114)
                                                ;   {section_word}
  0x00000000025245b6: jmp    0x0000000002524744
  0x00000000025245bb: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe1d]        # 0x00000000025243e0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@338 (line 112)
                                                ;   {section_word}
  0x00000000025245c3: jmp    0x0000000002524744
  0x00000000025245c8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe18]        # 0x00000000025243e8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@328 (line 110)
                                                ;   {section_word}
  0x00000000025245d0: jmp    0x0000000002524744
  0x00000000025245d5: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe13]        # 0x00000000025243f0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@318 (line 108)
                                                ;   {section_word}
  0x00000000025245dd: jmp    0x0000000002524744
  0x00000000025245e2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0e]        # 0x00000000025243f8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@308 (line 106)
                                                ;   {section_word}
  0x00000000025245ea: jmp    0x0000000002524744
  0x00000000025245ef: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe09]        # 0x0000000002524400
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@298 (line 104)
                                                ;   {section_word}
  0x00000000025245f7: jmp    0x0000000002524744
  0x00000000025245fc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe04]        # 0x0000000002524408
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@288 (line 102)
                                                ;   {section_word}
  0x0000000002524604: jmp    0x0000000002524744
  0x0000000002524609: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdff]        # 0x0000000002524410
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@278 (line 100)
                                                ;   {section_word}
  0x0000000002524611: jmp    0x0000000002524744
  0x0000000002524616: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdfa]        # 0x0000000002524418
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@268 (line 98)
                                                ;   {section_word}
  0x000000000252461e: jmp    0x0000000002524744
  0x0000000002524623: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffd9d]        # 0x00000000025243c8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@258 (line 96)
                                                ;   {section_word}
  0x000000000252462b: jmp    0x0000000002524744
  0x0000000002524630: movapd xmm0,xmm1
  0x0000000002524634: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0c]        # 0x0000000002524448
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@242 (line 92)
                                                ;   {section_word}
  0x000000000252463c: jmp    0x0000000002524744
  0x0000000002524641: movapd xmm0,xmm1
  0x0000000002524645: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffddb]        # 0x0000000002524428
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@236 (line 90)
                                                ;   {section_word}
  0x000000000252464d: jmp    0x0000000002524744
  0x0000000002524652: movapd xmm0,xmm1
  0x0000000002524656: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdd2]        # 0x0000000002524430
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@230 (line 88)
                                                ;   {section_word}
  0x000000000252465e: jmp    0x0000000002524744
  0x0000000002524663: movapd xmm0,xmm1
  0x0000000002524667: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdc9]        # 0x0000000002524438
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@224 (line 86)
                                                ;   {section_word}

[etc.]

  0x0000000002524744: add    rsp,0x10
  0x0000000002524748: pop    rbp
  0x0000000002524749: test   DWORD PTR [rip+0xfffffffffde1b8b1],eax        # 0x0000000000340000
                                                ;   {poll_return}
  0x000000000252474f: ret    

7
@ syb0rg Якщо чесно, я теж не розумію тонких деталей ;-)
assylias

4
+1 за чудову відповідь! Не могли б ви розібрати щось із 30+ випадків для порівняння, коли продуктивність закінчується "зануренням" у графіку ОП?
asteri


2
@AndrewBissell Моя здогадка полягає в тому, що інша поведінка заснована на (i) крос-тестах ефективності між архітектурою, які показали, що масив покажчиків є ефективним лише тоді, коли кількість випадків перевищує 18 або (ii) код профільовано як він запускається, і профайлер визначає, який підхід кращий під час виконання. Я не можу знайти відповідь.
assylias

3
Розбирання на 30 корпусів і 18 корпус виглядають здебільшого однаково. Відмінності здаються здебільшого обмеженими додатковим бітом додаткового переміщення регістру після приблизно 11-го випадку. Не можу сказати, чому JITter робить це; це видається непотрібним.
cHao

46

Перемикач - регістр відбувається швидше, якщо значення корпусу розміщуються у вузькому діапазоні Напр.

case 1:
case 2:
case 3:
..
..
case n:

Тому що в цьому випадку компілятор може уникати порівняння для кожної ноги випадку в операторі switch. Компілятор складає таблицю стрибків, яка містить адреси дій, які слід здійснити на різних ногах. Значення, на якому здійснюється перемикання, маніпулює, щоб перетворити його в індекс вjump table . У цій реалізації час, взятий у операторі комутатора, значно менше часу, який витрачається в еквіваленті каскаду операторів if-else-if. Також час, відведений в операторі перемикання, не залежить від кількості ніжок корпусу в операторі комутатора.

Як сказано у вікіпедії про оператор переключення в розділі "Компіляція".

Якщо діапазон вхідних значень ідентично "малий" і має лише кілька прогалин, деякі компілятори, що містять оптимізатор, можуть реально реалізувати оператор перемикання як таблицю гілки або масив індексованих функціональних покажчиків замість тривалої серії умовних інструкцій. Це дозволяє оператору перемикання миттєво визначати, яку гілку потрібно виконати, не проходячи список порівнянь.


4
це не правильно. Це буде швидше, незважаючи на те, що значення регістру вузькі або широкі в діапазоні. Це O (1) - не має значення, наскільки розділені величини регістру.
Aniket Inge

6
@Aniket: Прочитайте цю статтю у Вікіпедії. en.wikipedia.org/wiki/Branch_table
Vishal K

14
@Aniket: Це не O (1), якщо діапазон широкий і рідкий. Існує два типи комутаторів, і якщо діапазон занадто розширений, Java буде компілювати його до "lookupswitch", а не "tablewitch". Перша вимагає порівняння за галуззю до знайденої, а друга -.
cHao

4
Вікіпедія - гідне місце для пошуку посилань, але не слід вважати авторитетним джерелом. Все, що ви там читаєте, - у кращому випадку інформація про секонд-хенд.
cHao

6
@Aniket: Чесно кажучи, демонтаж є специфічним для даного JVM на певній платформі. Інші можуть перекласти це по-різному. Деякі фактично можуть використовувати хеш-таблицю для перегляду перемикача. Він все ще не буде працювати так добре, як настільний перемикач, але, принаймні, він може бути близьким. Це просто займе більше часу для JIT і передбачає застосування алгоритму хешування до введення. Отже, хоча отриманий код складання може бути освічуючим, він також не є авторитетним, якщо ви спеціально не говорите про Hotspot v1.7. що б там не було в Windows x86_64.
cHao

30

Відповідь лежить у байт-коді:

SwitchTest10.java

public class SwitchTest10 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 10: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

Відповідний байт-код; показані лише відповідні частини:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 10
        0: 60;
        1: 70;
        2: 80;
        3: 90;
        4: 100;
        5: 110;
        6: 120;
        7: 131;
        8: 142;
        9: 153;
        10: 164;
        default: 175 }

SwitchTest22.java:

public class SwitchTest22 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 100: System.out.println(10);
                    break;

            case 110: System.out.println(10);
                    break;
            case 120: System.out.println(10);
                    break;
            case 130: System.out.println(10);
                    break;
            case 140: System.out.println(10);
                    break;
            case 150: System.out.println(10);
                    break;
            case 160: System.out.println(10);
                    break;
            case 170: System.out.println(10);
                    break;
            case 180: System.out.println(10);
                    break;
            case 190: System.out.println(10);
                    break;
            case 200: System.out.println(10);
                    break;
            case 210: System.out.println(10);
                    break;

            case 220: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

Відповідний байт-код; знову показані лише відповідні частини:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   lookupswitch{ //23
        0: 196;
        1: 206;
        2: 216;
        3: 226;
        4: 236;
        5: 246;
        6: 256;
        7: 267;
        8: 278;
        9: 289;
        100: 300;
        110: 311;
        120: 322;
        130: 333;
        140: 344;
        150: 355;
        160: 366;
        170: 377;
        180: 388;
        190: 399;
        200: 410;
        210: 421;
        220: 432;
        default: 443 }

У першому випадку, з вузькими діапазонами, складений байт-код використовує a tableswitch. У другому випадку складений байт-код використовує a lookupswitch.

У tableswitch, ціле значення у верхній частині стека використовується для індексації в таблиці, для пошуку цілі гілки / стрибка. Потім цей стрибок / гілка виконується негайно. Отже, це O(1)операція.

А lookupswitchскладніше. У цьому випадку ціле значення потрібно порівнювати з усіма ключами таблиці, поки не буде знайдена правильна клавіша. Після того, як ключ знайдений, для стрибка використовується ціль гілки / стрибка (на яку цей ключ відображено). Таблиця, в якій використовується lookupswitch, сортується та алгоритм бінарного пошуку може бути використаний для пошуку правильного ключа. Продуктивність для двійкового пошуку є O(log n), і весь процес також є O(log n), тому що стрибок все ще O(1). Отже, при низьких діапазонах продуктивність знижується в тому, що спочатку потрібно шукати правильну клавішу, оскільки ви не можете безпосередньо проіндексувати таблицю.

Якщо є розріджені значення, і ви мали лише tableswitchвикористовувати, таблиця по суті міститиме фіктивні записи, які вказують на defaultпараметр. Наприклад, якщо припустити, що останній запис SwitchTest10.javaбуло 21замість 10, ви отримаєте:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 21
        0: 104;
        1: 114;
        2: 124;
        3: 134;
        4: 144;
        5: 154;
        6: 164;
        7: 175;
        8: 186;
        9: 197;
        10: 219;
        11: 219;
        12: 219;
        13: 219;
        14: 219;
        15: 219;
        16: 219;
        17: 219;
        18: 219;
        19: 219;
        20: 219;
        21: 208;
        default: 219 }

Тож компілятор в основному створює цю величезну таблицю, що містить фіктивні записи між пробілами, вказуючи на ціль гілки defaultінструкції. Навіть якщо немає default, він буде містити записи, що вказують на інструкцію після блоку комутації. Я зробив декілька основних тестів, і я виявив, що якщо розрив між останнім індексом та попереднім ( 9) більший за 35, він використовує lookupswitchзамість а tableswitch.

Поведінка switchоператора визначається у специфікації Java Virtual Machine (§3.10) :

Якщо випадки перемикання є рідкісними, представлення таблиць інструкції перемикача таблиць стає неефективним щодо простору. Натомість може використовуватися інструкція lookupswitch. Інструкція lookupswitch з’єднує клавіші int (значення міток регістру) з цільовими зміщеннями в таблиці. Коли виконується інструкція lookupswitch, значення виразу перемикача порівнюється з клавішами в таблиці. Якщо одна з клавіш відповідає значенню виразу, виконання продовжується при пов'язаному зміщенні цілі. Якщо жоден ключ не збігається, виконання триває за цільовим завданням. [...]


1
З питання я зрозумів, що цифри завжди суміжні, але діапазон є більш-менш довгим, тобто в одному прикладі випадки переходять від 0 до 5, тоді як в іншому прикладі вони переходять від 0 до 30 - і жоден з прикладів не використовує рідкісні значення
assylias

@assylias Хм, цікаво. Напевно, я неправильно зрозумів питання. Дозвольте мені ще кілька експериментів. Таким чином , ви хочете сказати , що навіть з прилеглим діапазоном від 0-30, компілятор використовує lookupswitch?
Вівін Паліяф

@VivinPaliath: Так, у моїх тестах константи випадку завжди суміжні, тому я в основному тестую перемикачі на [0, 1], [0, 1, 2], [0, 1, 2, 3] ... і т.д.
Ендрю Бісселл

@VivinPaliath Ні, байт-код завжди використовує табличний перемикач - однак компілятор JIT, схоже, не збирає таблицю перемикача для збірки однаково, залежно від того, скільки елементів він містить.
assylias

6
@VivinPaliath Я міг би чітко сформулювати це питання напевно. Мені щось не вдається з точки зору глибини, коли мова йде про оцінку відповідей, що стосуються цього низькорівневого байт-коду та складання матеріалів. Мені все ще здається, що на сьогодні насправді важливим є розрізнення таблиціwitch / lookupswitch, і ваша єдина відповідь, яка використовує ці терміни поки що (хоча інші, мабуть, викладають ту саму концепцію з різною термінологією). Плюс мені також подобається, що також є посилання JVM Spec.
Ендрю Бісселл

19

Оскільки на запитання вже відповіли (більш-менш), ось деяка порада. Використовуйте

private static final double[] mul={1d, 10d...};
static double multiplyByPowerOfTen(final double d, final int exponent) {
      if (exponent<0 || exponent>=mul.length) throw new ParseException();//or just leave the IOOBE be
      return mul[exponent]*d;
}

Цей код використовує значно менше ІС (кеш інструкцій) і завжди буде вкладений. Масив буде в кеші даних L1, якщо код гарячий. Таблиця пошуку майже завжди є виграшною. (особливо про мікро-показники: D)

Редагувати: якщо ви хочете, щоб метод був гарячим вкладеним, розгляньте нешвидкі шляхи, як, наприклад throw new ParseException(), короткі як мінімум, або перенесіть їх на окремий статичний метод (отже, зробіть їх короткими як мінімальні). Це throw new ParseException("Unhandled power of ten " + power, 0);слабка ідея, оскільки вона з'їдає велику кількість вбудованого бюджету для коду, який можна просто інтерпретувати - конкатенація рядків є досить багатослівною у байт-коді. Більше інформації та реальний випадок з ArrayList

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.