Почему переключение быстрее, чем если бы

116

Во многих книгах по Java switchоператор описывается как более быстрый, чем if elseоператор. Но нигде не узнал, почему switch быстрее if .

пример

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

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

или

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

учитывая элемент, а ХЛЕБ - постоянное значение типа int.

Что в приведенном выше примере работает быстрее и почему?

Якоб ван Линген
источник
Может быть, это ответ и для java: stackoverflow.com/questions/767821/…
Тобиас
19
В общем, из Википедии : если диапазон входных значений идентифицируемо «мал» и имеет лишь несколько пробелов, некоторые компиляторы, которые включают оптимизатор, могут фактически реализовать оператор switch как таблицу ветвления или массив указателей индексированных функций вместо длинный ряд условных инструкций. Это позволяет оператору switch мгновенно определить, какую ветвь выполнить, без необходимости просматривать список сравнений.
Felix Kling
Главный ответ на этот вопрос очень хорошо это объясняет. Эта статья тоже все очень хорошо объясняет.
bezmax
Я надеюсь, что в большинстве случаев оптимизирующий компилятор сможет сгенерировать код с аналогичными характеристиками производительности. В любом случае вам придется звонить много миллионов раз, чтобы заметить разницу.
Mitch Wheat
2
Вам следует с осторожностью относиться к книгам, которые делают подобные утверждения без объяснений / доказательств / аргументов.
matt b

Ответы:

110

Потому что существуют специальные байт-коды, которые позволяют эффективно оценивать оператор switch при большом количестве случаев.

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

Даниил
источник
6
Разве итерация не переводится как «проверка, прыжок»?
fivetwentysix
17
@fivetwentysix: Нет, для получения информации обратитесь к этому: artima.com/underthehood/flowP.html . Цитата из статьи: Когда JVM встречает инструкцию tablewitch, она может просто проверить, находится ли ключ в пределах диапазона, определяемого low и high. В противном случае используется смещение ветви по умолчанию. Если это так, он просто вычитает нижнюю часть ключа, чтобы получить смещение в списке смещений ветвей. Таким образом, он может определить соответствующее смещение ветвления без необходимости проверять значение каждого варианта.
bezmax
1
(i) a switchне может быть преобразовано в tableswitchинструкцию байт-кода - она ​​может стать lookupswitchинструкцией, которая работает аналогично if / else (ii) даже tableswitchинструкции байт-кода могут быть скомпилированы JIT в серию if / else в зависимости от факторов например, количество cases.
assylias
1
tableswitchvs loopuswitch: stackoverflow.com/questions/10287700/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
34

switchУтверждение не всегда быстрее , чем ifзаявление. Он масштабируется лучше, чем длинный список if-elseоператоров, так как switchможет выполнять поиск по всем значениям. Однако для кратковременных условий это не будет быстрее, а может быть медленнее.

Питер Лоури
источник
5
Пожалуйста, ограничьте "долго". Больше 5? Больше 10? или больше вроде 20 - 30?
vanderwyst
11
Я подозреваю, это зависит от обстоятельств. Для меня 3 или более предложений switchбыли бы яснее, если не быстрее.
Питер Лоури
В каких условиях он мог быть медленнее?
Эрик
1
@Eric медленнее для небольшого количества значений, например String или int, которые являются разреженными.
Питер Лоури 03
8

Текущая JVM имеет два типа байтовых кодов переключателей: LookupSwitch и TableSwitch.

Каждый case в операторе switch имеет целочисленное смещение, если эти смещения являются смежными (или в основном смежными без больших промежутков) (case 0: case 1: case 2, и т. Д.), То используется TableSwitch.

Если смещения разбросаны с большими промежутками (case 0: case 400: case 93748: и т. Д.), То используется LookupSwitch.

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

Переключатель поиска использует двоичный поиск для поиска правильной ветки кода. Это выполняется за время O (log n), что все еще хорошо, но не лучше.

Дополнительные сведения об этом см. Здесь: Разница между LookupSwitch и TableSwitch JVM?

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

Если у вас 2 случая, используйте оператор if.

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

Также имейте в виду, что JVM будет запускать JIT-оптимизацию для операторов if, которые будут пытаться разместить самую горячую ветвь первой в коде. Это называется «Прогнозирование ветвления». Для получения дополнительной информации об этом см. Здесь: https://dzone.com/articles/branch-prediction-in-java.

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

HesNotTheStig
источник
1
После публикации этого сообщения я обратил внимание на то, что «выражения переключения» и «сопоставление с образцом» появятся в Java, возможно, сразу после выхода Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Пока нет ничего конкретного, но похоже, что они сделают switchеще более мощную языковую функцию. Например, сопоставление с образцом обеспечит более плавный и эффективный instanceofпоиск. Однако я думаю, что можно с уверенностью предположить, что для базовых сценариев switch / if указанное мной правило будет по-прежнему применяться.
HesNotTheStig
1

Так что, если вы планируете иметь много пакетов, память в наши дни не является большой затратой, а массивы работают довольно быстро. Вы также не можете полагаться на оператор switch для автоматического создания таблицы переходов, и поэтому проще создать сценарий таблицы переходов самостоятельно. Как вы можете видеть в приведенном ниже примере, мы предполагаем максимум 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 как наихудший сценарий.

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

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, загруженном средой выполнения, и это в операторе switch; тогда как в операторе if другая инструкция jvm создается компилирующим код DE, и для этого требуется, чтобы каждая переменная была загружена в регистры, хотя используется та же переменная, что и в следующем предыдущем операторе if. Если вы знаете кодирование на языке ассемблера, это было бы обычным делом; хотя java-скомпилированные coxes не являются байт-кодом или прямым машинным кодом, условная концепция здесь по-прежнему согласована. Что ж, при объяснении я попытался избежать более глубоких технических деталей. Надеюсь, я прояснил эту концепцию и развенчал ее тайну. Спасибо.

Стих Вильялон Гамбоа
источник