Чому перехід швидше, ніж якщо


116

Багато книг Java описують switchтвердження як швидше, ніж if elseтвердження. Але я ніде не з’ясував, чому перемикання швидше, ніж якби .

Приклад

У мене є ситуація, я повинен вибрати один із двох предметів. Я можу використовувати будь-яке використання

switch (item) {
    case BREAD:
        //eat Bread
        break;
    default:
        //leave the restaurant
}

або

if (item == BREAD) {
    //eat Bread
} else {
    //leave the restaurant
}

враховуючи предмет та BREAD - це постійне значення int.

У наведеному вище прикладі, що швидше діє і чому?


Можливо, це відповідь і для java: stackoverflow.com/questions/767821/…
Тобіас

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

Верхня відповідь на це запитання пояснює це досить добре. Ця стаття також пояснює все досить добре.
bezmax

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

2
Ви повинні насторожено ставитися до книг, які роблять подібні заяви без пояснень / доказів / міркувань.
matt b

Відповіді:


110

Оскільки є спеціальні байт-коди, які дозволяють ефективно оцінювати оператори переключення, коли випадків дуже багато.

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


6
Чи не повторюється переклад "перевірити, стрибати"?
Fivetwentysix

17
@fivetwentysix: Ні, зверніться до цього, щоб отримати інформацію: artima.com/underthehood/flowP.html . Цитата із статті: Коли JVM стикається з інструкцією табличного перемикача, він може просто перевірити, чи ключ знаходиться в межах, визначених низьким і високим. Якщо ні, то це зсув гілки за замовчуванням. Якщо це так, він просто віднімає низький показник від ключа, щоб отримати зміщення у списку компенсацій гілок. Таким чином, він може визначити відповідне зміщення гілки, не перевіряючи значення кожного випадку.
bezmax

1
(i) а) switchне може бути переведена в tableswitchінструкцію щодо байт-коду - вона може стати lookupswitchінструкцією, яка виконує аналогічно if / else (ii) навіть tableswitchінструкції байтового коду можуть бути складені в серії if / else JIT, залежно від факторів наприклад кількість cases.
assylias


34

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


5
Будь ласка, обмежуйтеся "довго". Більше 5? Більше 10? або більше, як 20 - 30?
vanderwyst

11
Я підозрюю, що це залежить. Для мене його 3 і більше пропозицій switchбуло б зрозуміліше, якби не швидше.
Пітер Лорі

За яких умов це може бути повільніше?
Ерік

1
@Eric це повільніше для невеликої кількості значень esp String або int, які є рідкими.
Пітер Лорі

8

У поточному JVM є два види байтових кодів комутаторів: LookupSwitch та TableSwitch.

Кожен випадок у операторі перемикання має ціле зміщення, якщо ці зміщення є суміжними (або здебільшого суміжними без великих проміжків) (випадок 0: випадок 1: випадок 2 тощо), тоді використовується TableSwitch.

Якщо зсуви розподіляються з великими прогалинами (випадок 0: випадок 400: випадок 93748:, тощо), тоді використовується LookupSwitch.

Коротше кажучи, різниця полягає в тому, що TableSwitch виконується в постійний час, оскільки кожному значенню в межах можливих значень надається певне зміщення байтового коду. Таким чином, коли ви даєте заяві зміщення 3, воно знає, що потрібно стрибнути вперед 3, щоб знайти правильну гілку.

Перемикач пошуку використовує двійковий пошук, щоб знайти правильну гілку коду. Це працює в O (log n) час, що все ще добре, але не найкраще.

Більш детальну інформацію про це дивіться тут: Різниця між LookupSwitch JVM та TableSwitch?

Що стосується того, який із них найшвидший, використовуйте такий підхід: Якщо у вас є 3 або більше випадків, значення яких послідовні або майже послідовні, завжди використовуйте комутатор.

Якщо у вас є 2 випадки, використовуйте оператор if.

У будь-якій іншій ситуації перемикання швидше за все швидше, але це не гарантується, оскільки двійковий пошук у LookupSwitch може спричинити поганий сценарій.

Крім того, майте на увазі, що JVM запустить оптимізацію JIT, якщо в коді будуть заявки, які намагатимуться першими поставити найгарячішу гілку. Це називається "Галузеве передбачення". Детальніше про це дивіться тут: https://dzone.com/articles/branch-prediction-in-java

Ваш досвід може відрізнятися. Я не знаю, що JVM не проводить подібної оптимізації на LookupSwitch, але я навчився довіряти оптимізації JIT і не намагатися перехитрити компілятор.


1
Після публікації цього повідомлення мені стало відомо, що "перемикання виразів" та "відповідність шаблону" надходять на Java, можливо, як тільки Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Ніщо не є конкретним, але, схоже, це зробить switchще більш потужною особливістю мови. Наприклад, відповідність шаблонів дозволить зробити більш плавні та ефективні instanceofпошуки. Однак я вважаю, що можна припустити, що для базових сценаріїв перемикання / якщо сценарії, які я згадав, все ще застосовуватимуться.
HesNotTheStig

1

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

Щоб отримати нижченаведений результат, вам потрібна абстракція. Я не збираюся пояснювати, як це працює так, сподіваюся, ви розумієте це.

Я оновив це, щоб встановити розмір пакета на 255, якщо вам потрібно більше, то вам доведеться зробити перевірку меж (id <0) || (id> довжина).

Packets[] packets = new Packets[255];

static {
     packets[0] = new Login(6);
     packets[2] = new Logout(8);
     packets[4] = new GetMessage(1);
     packets[8] = new AddFriend(0);
     packets[11] = new JoinGroupChat(7); // etc... not going to finish.
}

public void handlePacket(IncomingData data)
{
    int id = data.readByte() & 0xFF; //Secure value to 0-255.

    if (packet[id] == null)
        return; //Leave if packet is unhandled.

    packets[id].execute(data);
}

Редагувати, оскільки я багато використовую таблицю стрибків у C ++, я зараз покажу приклад таблиці функції стрибків вказівника. Це дуже загальний приклад, але я його запустив, і він працює правильно. Майте на увазі, що ви повинні встановити покажчик на NULL, C ++ не зробить це автоматично, як у Java.

#include <iostream>

struct Packet
{
    void(*execute)() = NULL;
};

Packet incoming_packet[255];
uint8_t test_value = 0;

void A() 
{ 
    std::cout << "I'm the 1st test.\n";
}

void B() 
{ 
    std::cout << "I'm the 2nd test.\n";
}

void Empty() 
{ 

}

void Update()
{
    if (incoming_packet[test_value].execute == NULL)
        return;

    incoming_packet[test_value].execute();
}

void InitializePackets()
{
    incoming_packet[0].execute = A;
    incoming_packet[2].execute = B;
    incoming_packet[6].execute = A;
    incoming_packet[9].execute = Empty;
}

int main()
{
    InitializePackets();

    for (int i = 0; i < 512; ++i)
    {
        Update();
        ++test_value;
    }
    system("pause");
    return 0;
}

Ще один момент, який я хотів би підняти, - це знаменитий поділ і підкорення. Тож моя вище 255 ідея масиву може бути зведена до не більше 8, якщо заяви як найгірший сценарій.

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

If (Value >= 128)
{
   if (Value >= 192)
   {
        if (Value >= 224)
        {
             if (Value >= 240)
             {
                  if (Value >= 248)
                  {
                      if (Value >= 252)
                      {
                          if (Value >= 254)
                          {
                              if (value == 255)
                              {

                              } else {

                              }
                          }
                      }
                  }
             }      
        }
   }
}

2
Чому подвійне непряме? Оскільки ідентифікатор повинен бути обмежений у будь-якому випадку, чому б не просто встановити межі - перевірити вхідний ідентифікатор як 0 <= id < packets.lengthі забезпечити, packets[id]!=nullа потім зробити packets[id].execute(data)?
Лоуренс Дол

так, вибачте за пізню відповідь, переглянувши це ще раз .. і я не маю уявлення про те, що чорт я думав, я оновив повідомлення lol і обмежив пакети на розмір неподписаного байта, тому не потрібно перевіряти довжину.
Джеремі Трифіло

0

На рівні байт-коду тематична змінна завантажується лише один раз в регістр процесора з адреси пам'яті в структурованому файлі .class, завантаженому Runtime, і це в операторі комутатора; тоді як в операторі if-if ваша команда DE, що компілює код, виробляє іншу інструкцію jvm, і для цього потрібно, щоб кожна змінна була завантажена в регістри, хоча використовується така ж змінна, як і в наступному перед оператором if-statement. Якщо ви знаєте кодування в мові збірки, то це було б звичним явищем; Хоча java, складений coxes, не є байт-кодом або прямим машинним кодом, умовна концепція цього документа все ще є узгодженою. Ну, я намагався уникати більш глибокої технічності після пояснення. Я сподіваюся, що я зробив цю концепцію зрозумілою та демістифікованою. Дякую.

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