Является ли «переключатель» быстрее, чем «если»? - PullRequest
230 голосов
/ 24 июля 2011

Является ли оператор switch на самом деле быстрее, чем оператор if?

Я запустил приведенный ниже код на x64 C ++ компиляторе Visual Studio 2010 с флагом /Ox:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 29)
size_t counter = 0;

size_t testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        switch (counter % 4 + 1)
        {
            case 1: counter += 4; break;
            case 2: counter += 3; break;
            case 3: counter += 2; break;
            case 4: counter += 1; break;
        }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

size_t testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = counter % 4 + 1;
        if (c == 1) { counter += 4; }
        else if (c == 2) { counter += 3; }
        else if (c == 3) { counter += 2; }
        else if (c == 4) { counter += 1; }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    printf("Starting...\n");
    printf("Switch statement: %u ms\n", testSwitch());
    printf("If     statement: %u ms\n", testIf());
}

и получил эти результаты:

Оператор переключения: 5261 мс
Если утверждение: 5196 мс

Из того, что я узнал, операторы switch, очевидно, используют таблицы переходов для оптимизации ветвления.

Вопросы:

  1. Как будет выглядеть базовая таблица переходов в x86 или x64?

  2. Использует ли этот код таблицу переходов?

  3. Почему в этом примере нет разницы в производительности? Есть ли ситуация, в которой является существенной разницей в производительности?


Разборка кода:

testIf:

13FE81B10 sub  rsp,48h 
13FE81B14 call qword ptr [__imp_clock (13FE81128h)] 
13FE81B1A mov  dword ptr [start],eax 
13FE81B1E mov  qword ptr [i],0 
13FE81B27 jmp  testIf+26h (13FE81B36h) 
13FE81B29 mov  rax,qword ptr [i] 
13FE81B2E inc  rax  
13FE81B31 mov  qword ptr [i],rax 
13FE81B36 cmp  qword ptr [i],20000000h 
13FE81B3F jae  testIf+0C3h (13FE81BD3h) 
13FE81B45 xor  edx,edx 
13FE81B47 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B4E mov  ecx,4 
13FE81B53 div  rax,rcx 
13FE81B56 mov  rax,rdx 
13FE81B59 inc  rax  
13FE81B5C mov  qword ptr [c],rax 
13FE81B61 cmp  qword ptr [c],1 
13FE81B67 jne  testIf+6Dh (13FE81B7Dh) 
13FE81B69 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B70 add  rax,4 
13FE81B74 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B7B jmp  testIf+0BEh (13FE81BCEh) 
13FE81B7D cmp  qword ptr [c],2 
13FE81B83 jne  testIf+89h (13FE81B99h) 
13FE81B85 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B8C add  rax,3 
13FE81B90 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B97 jmp  testIf+0BEh (13FE81BCEh) 
13FE81B99 cmp  qword ptr [c],3 
13FE81B9F jne  testIf+0A5h (13FE81BB5h) 
13FE81BA1 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BA8 add  rax,2 
13FE81BAC mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BB3 jmp  testIf+0BEh (13FE81BCEh) 
13FE81BB5 cmp  qword ptr [c],4 
13FE81BBB jne  testIf+0BEh (13FE81BCEh) 
13FE81BBD mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BC4 inc  rax  
13FE81BC7 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BCE jmp  testIf+19h (13FE81B29h) 
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)] 
13FE81BD9 sub  eax,dword ptr [start] 
13FE81BDD imul eax,eax,3E8h 
13FE81BE3 cdq       
13FE81BE4 mov  ecx,3E8h 
13FE81BE9 idiv eax,ecx 
13FE81BEB cdqe      
13FE81BED add  rsp,48h 
13FE81BF1 ret       

testSwitch:

13FE81C00 sub  rsp,48h 
13FE81C04 call qword ptr [__imp_clock (13FE81128h)] 
13FE81C0A mov  dword ptr [start],eax 
13FE81C0E mov  qword ptr [i],0 
13FE81C17 jmp  testSwitch+26h (13FE81C26h) 
13FE81C19 mov  rax,qword ptr [i] 
13FE81C1E inc  rax  
13FE81C21 mov  qword ptr [i],rax 
13FE81C26 cmp  qword ptr [i],20000000h 
13FE81C2F jae  testSwitch+0C5h (13FE81CC5h) 
13FE81C35 xor  edx,edx 
13FE81C37 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C3E mov  ecx,4 
13FE81C43 div  rax,rcx 
13FE81C46 mov  rax,rdx 
13FE81C49 inc  rax  
13FE81C4C mov  qword ptr [rsp+30h],rax 
13FE81C51 cmp  qword ptr [rsp+30h],1 
13FE81C57 je   testSwitch+73h (13FE81C73h) 
13FE81C59 cmp  qword ptr [rsp+30h],2 
13FE81C5F je   testSwitch+87h (13FE81C87h) 
13FE81C61 cmp  qword ptr [rsp+30h],3 
13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
13FE81C69 cmp  qword ptr [rsp+30h],4 
13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 
13FE81C71 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C73 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C7A add  rax,4 
13FE81C7E mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C85 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C87 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C8E add  rax,3 
13FE81C92 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C99 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C9B mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CA2 add  rax,2 
13FE81CA6 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CAD jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81CAF mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CB6 inc  rax  
13FE81CB9 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CC0 jmp  testSwitch+19h (13FE81C19h) 
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)] 
13FE81CCB sub  eax,dword ptr [start] 
13FE81CCF imul eax,eax,3E8h 
13FE81CD5 cdq       
13FE81CD6 mov  ecx,3E8h 
13FE81CDB idiv eax,ecx 
13FE81CDD cdqe      
13FE81CDF add  rsp,48h 
13FE81CE3 ret       

Обновление:

Интересные результаты здесь . Не уверен, почему один быстрее, а другой медленнее.

Ответы [ 12 ]

117 голосов
/ 24 июля 2011

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

C Псевдокод для «таблицы переходов» будетчто-то наподобие this - обратите внимание, что на практике компилятору потребуется вставить некоторую форму проверки if вокруг таблицы, чтобы убедиться, что входные данные верны в таблице.Также обратите внимание, что это работает только в конкретном случае, когда ввод представляет собой последовательность последовательных чисел.

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

Чтобы ответить на ваши конкретные вопросы:

  1. Clang генерирует такую, которая выглядит как this :

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
    
  2. Могу сказать, что он не использует таблицу переходов - четко видны 4 инструкции сравнения:

    13FE81C51 cmp  qword ptr [rsp+30h],1 
    13FE81C57 je   testSwitch+73h (13FE81C73h) 
    13FE81C59 cmp  qword ptr [rsp+30h],2 
    13FE81C5F je   testSwitch+87h (13FE81C87h) 
    13FE81C61 cmp  qword ptr [rsp+30h],3 
    13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
    13FE81C69 cmp  qword ptr [rsp+30h],4 
    13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 
    

    Решение на основе таблицы переходов вообще не использует сравнение.

  3. Либо недостаточно ветвей, чтобы компилятор генерировал таблицу переходов, либо ваш компилятор просто не генерирует их.Я не уверен, какой именно.

РЕДАКТИРОВАТЬ 2014 : в других местах обсуждались люди, знакомые с оптимизатором LLVM, которые говорили, что оптимизация таблицы переходов может быть важной во многих сценариях.;например, в случаях, когда существует перечисление со многими значениями и во многих случаях со значениями в указанном перечислении.Тем не менее, я придерживаюсь того, что я сказал выше в 2011 году - слишком часто я вижу, что люди думают: «Если я сделаю это переключение, это будет то же самое время, независимо от того, сколько у меня дел» - и это совершенно неверно.Даже с таблицей переходов вы получаете косвенную стоимость перехода и платите за записи в таблице для каждого случая;и пропускная способность памяти - это большое дело для современного оборудования.

Пишите код для удобства чтения. Любой компилятор, достойный своей соли, увидит лестницу if / else if и преобразует ее в эквивалентный переключатель или наоборот, если это будет быстрее.

43 голосов
/ 24 июля 2011

На ваш вопрос:

1. Как будет выглядеть базовая таблица переходов в x86 или x64?

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

00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

enter image description here

Где 00B14538 - указатель на переходtable, а значение типа D8 09 AB 00 представляет указатель метки.

2. Использует ли этот код таблицу переходов? Нет в этом случае.

3.Почему в этом примере нет разницы в производительности?

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

4.Есть ли ситуация, в которой есть существенная разница в производительности?

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

Код для всех инструкций сравнения тоже имеет некоторый размертак что особенно с 32-битными указателями или смещениями, один Jпоиск в таблице ump может не стоить намного большего размера в исполняемом файле.

Вывод: компилятор достаточно умен, обрабатывает такие случаи и генерирует соответствующие инструкции:)

31 голосов
/ 24 июля 2011

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

Я бы доверил компилятору сделать лучший выбор и сосредоточился на том, что делает код наиболее читабельным.

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

13 голосов
/ 24 июля 2011

Откуда вы знаете, что ваш компьютер не выполнял какую-либо задачу, не связанную с тестом во время цикла тестирования коммутатора, и выполнял меньше задач во время цикла проверки if? Результаты вашего теста не показывают ничего как:

  1. разница очень мала
  2. есть только один результат, а не серия результатов
  3. слишком мало дел

Мои результаты:

Я добавил:

printf("counter: %u\n", counter);

до конца, чтобы он не оптимизировал цикл, так как в вашем примере счетчик никогда не использовался, так почему компилятор выполняет цикл? Сразу же, коммутатор всегда выигрывал даже при таком микро-бенчмарке.

Другая проблема с вашим кодом:

switch (counter % 4 + 1)

в вашей петле переключателя, против

const size_t c = counter % 4 + 1; 

в вашем цикле if. Очень большая разница, если вы это исправите. Я считаю, что размещение оператора внутри оператора switch заставляет компилятор отправлять значение непосредственно в регистры ЦП, а не помещать его в стек первым. Поэтому это в пользу оператора switch, а не сбалансированного теста.

О, и я думаю, вам также следует сбросить счетчик между тестами. На самом деле, вы, вероятно, должны использовать какое-то случайное число вместо +1, +2, +3 и т. Д., Так как оно, вероятно, что-то там оптимизирует. Например, под случайным числом подразумевается число, основанное на текущем времени. В противном случае компилятор может превратить обе ваши функции в одну длинную математическую операцию и даже не беспокоиться ни о каких циклах.

Я изменил код Райана настолько, чтобы компилятор не мог разобраться до запуска кода:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 26)
size_t counter = 0;

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

переключатель: 3740
если: 3980

(аналогичные результаты при многократных попытках)

Я также уменьшил количество дел / случаев до 5, и функция переключения все еще выиграла.

7 голосов
/ 25 июля 2011

Хороший оптимизирующий компилятор, такой как MSVC, может генерировать:

  1. простая таблица переходов, если кейсы расположены на большом расстоянии
  2. таблица разреженных (двухуровневых) прыжков, если есть много пробелов
  3. серия ifs, если число дел мало или значения не близко друг к другу
  4. комбинация выше, если случаи представляют несколько групп близко расположенные диапазоны.

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

5 голосов
/ 24 июля 2011

Я отвечу 2) и сделаю несколько общих замечаний.2) Нет, в коде сборки, который вы разместили, нет таблицы переходов.Таблица переходов - это таблица пунктов назначения перехода и одна или две инструкции для перехода непосредственно в индексированное местоположение из таблицы.Таблица переходов имеет больше смысла, когда есть много возможных пунктов назначения переключения.Может быть, оптимизатор знает, что простая, если еще логика быстрее, если количество пунктов назначения не превышает некоторый порогПопробуйте еще раз, скажем, 20 вариантов вместо 4.

4 голосов
/ 24 июля 2011

Я был заинтригован и посмотрел на то, что я мог бы изменить в вашем примере, чтобы он быстрее выполнял оператор switch.

Если вы получите 40 операторов if и добавите регистр 0, тогда блок if будет работать медленнее, чем эквивалентный оператор switch.У меня есть результаты здесь: https://www.ideone.com/KZeCz.

Эффект удаления случая 0 можно увидеть здесь: https://www.ideone.com/LFnrX.

3 голосов
/ 26 июля 2012

Вот некоторые результаты из старого (сейчас трудно найти) теста Bench ++:

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

Из этого видно, что (на этой машине с этим компилятором - VC ++ 9.0 x64) каждый if тест занимает около 0,7 наносекунды. По мере увеличения числа испытаний время масштабируется почти идеально линейно.

С оператором switch, почти нет разницы в скорости между 2-х и 10-ти сторонним тестом, если значения плотные. Тест с 10 путями с разреженными значениями занимает примерно в 1,6 раза больше времени, чем тест с 10 путями с плотными значениями, но даже при разреженных значениях он все же лучше, чем удвоенная скорость с 10 путями if / else if .

Итог: использование только 4-стороннего теста на самом деле не покажет вам много о производительности switch против if / else. Если вы посмотрите на числа из этого кода, то довольно просто интерполировать тот факт, что для 4-стороннего теста мы ожидаем, что эти два результата дадут довольно аналогичных результатов (~ 2,8 наносекунды для if / else, ~ 2,0 для switch).

2 голосов
/ 01 октября 2011

Обратите внимание, что когда переключатель НЕ компилируется в таблицу переходов, вы можете очень часто писать, если он более эффективен, чем переключатель ...

(1) если дела имеют порядок, а не наихудшее тестирование для всех N, вы можете написать свои if для проверки, находится ли в верхней или нижней половине, а затем в каждой половине этого стиля двоичного поиска. в худшем случае это logN, а не N

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

2 голосов
/ 25 июля 2011

Не уверен, почему кто-то быстрее, а кто-то медленнее.

Это на самом деле не так уж сложно объяснить ... Если вы помните, что ошибочно предсказанные ветви - это десятки и сотнив разы дороже, чем правильно спрогнозированные ветви.

В версии % 20 первый случай / if всегда тот, который попадает.Современные процессоры «узнают», какие ветви обычно используются, а какие нет, поэтому они могут легко предсказать, как эта ветвь будет вести себя почти на каждой итерации цикла.Это объясняет, почему вылетает версия «если»;он никогда не должен выполнять что-либо после первого теста, и он (правильно) предсказывает результат этого теста для большинства итераций.Очевидно, что «переключатель» реализован немного иначе - возможно, даже таблица переходов, которая может быть медленной благодаря вычисляемой ветви.

В версии % 21 ветви по существу случайны.Так что не только многие из них выполняют каждую итерацию, процессор не может угадать, в каком направлении они пойдут.Это тот случай, когда таблица переходов (или другая оптимизация «переключателя») может помочь.

Очень трудно предсказать, как часть кода будет работать с современным компилятором и процессором, ис каждым поколением становится все сложнее.Лучший совет - «даже не пытайся, всегда в профиле».Этот совет становится лучше - и число людей, которые могут его игнорировать, с каждым годом становится все меньше.

Все это говорит о том, что мое объяснение, приведенное выше, в значительной степени является предположением.: -)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...