Чи "перемикання" швидше, ніж "якщо"?


242

Чи 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       

Оновлення:

Цікаві результати тут . Не впевнений, чому один швидше, а інший - повільніше.


47
Що на Землі люди голосують, щоб закрити це мислення? Чи вони такі віруючі в ідею ідеально оптимізуючого компілятора, що будь-яка думка про його генерування менше ідеального коду є єресі? Є чи сама ідея будь оптимізації в будь-якому місці їх ображати?
Crashworks

6
Що саме не так у цьому питанні?
Тугрул Атес

25
Для тих , цікаво , що сталося з цим питанням : У - перших, це не питання, це 3 питань, а це значить , що багато хто з відповідей в даний час різні питання. Це означає, що важко буде прийняти будь-яку відповідь, яка відповідає на все . Крім того, типова реакція на коліна на вищезазначене питання полягає в тому, щоб закрити її як "не дуже цікаву", головним чином через те, що на такому рівні оптимізації ви майже завжди передчасно оптимізуєте . Нарешті, 5196 порівняно з 5261 не повинно бути достатньо, щоб насправді піклуватися. Напишіть логічний код, який має сенс.
Лассе В. Карлсен

40
@Lasse: Ви дійсно хотіли б, щоб я замість цього розмістив три питання? Крім того : 5196 vs. 5261 shouldn't be enough to actually care-> Я не впевнений , якщо ви НЕ зрозуміли питання або , якщо я неправильно зрозуміти ваш коментар, але не вся суть мого питання , щоб запитати , чому НЕ різниця? (Чи коли-небудь я стверджував, що це важлива різниця?)
користувач541686,

5
@Robert: Ну, це лише більше 20 коментарів до нього, оскільки вони мета-коментарі. Тут є лише 7 коментарів, які насправді пов'язані з питанням. Думка: Я не бачу, як тут існує "думка". Є причина, що я не бачу різниці в продуктивності, ні? Це просто смак? Дебати: Може, але це виглядає як здоровий вид дискусії для мене, як я бачив в інших місцях на SO (дайте мені знати, чи є щось протилежне цьому). Аргументи: я не бачу тут нічого аргументативного (якщо ви не сприймаєте це як синонім «дискусії»?). Розширене обговорення: якщо ви включите ці мета-коментарі.
користувач541686

Відповіді:


122

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

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

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

Щоб відповісти на ваші конкретні запитання:

  1. Clang генерує той , який виглядає , як це :

    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. Або недостатньо гілок, щоб змусити компілятор створити таблицю стрибків, або ваш компілятор просто не генерує їх. Я не впевнений, який.

EDIT 2014 : в інших місцях обговорювались люди, знайомі з оптимізатором LLVM, кажучи, що оптимізація таблиці стрибків може бути важливою у багатьох сценаріях; наприклад, у випадках, коли існує перерахування з великою кількістю значень і багато випадків проти значень у зазначеному перерахуванні. Однак, я стояв за те, що я говорив вище в 2011 році - занадто часто я бачу людей, які думають, "якщо я переключу його, це буде той самий час, незалежно від того, скільки випадків у мене є" - і це абсолютно помилково. Навіть за допомогою таблиці стрибків ви отримуєте непряму вартість стрибка і ви оплачуєте записи в таблиці для кожного випадку; і пропускна здатність пам'яті - це велика пропозиція на сучасному обладнанні.

Напишіть код для читабельності. Будь-який компілятор, що коштує його солі, збирається побачити if / else, якщо сходи, і перетворить її на еквівалентний перемикач або навпаки, якщо це буде швидше зробити це.


3
+1 за фактичну відповідь на запитання та корисну інформацію. :-) Однак питання: з того, що я розумію, таблиця стрибків використовує непрямі стрибки; це правильно? Якщо так, то чи не все це повільніше через складніші попередні вилучення / конвеєринг?
користувач541686

1
@Mehrdad: Так, він використовує непрямі стрибки. Однак один непрямий стрибок (із затримкою трубопроводу, з яким він йде) може бути меншим, ніж сотні прямих стрибків. :)
Біллі ONeal

1
@Mehrdad: Ні, на жаль. :( Я радий, що перебуваю в таборі людей, які завжди думають, що ІФ є більш читабельним! :)
Біллі ONeal

1
Кілька кроків - "[перемикачі] працюють лише тоді, коли вхід може бути обмежений якимось чином" "потрібно вставити якусь форму, якщо тест навколо таблиці, щоб переконатися, що введення було дійсним у таблиці. Зауважте також, що він працює лише в конкретних у випадку, якщо вхід - це цикл послідовних чисел. ": цілком можливо мати малонаселену таблицю, де зчитується потенційний вказівник, і лише якщо не-NULL є стрибком, інакше випадок за замовчуванням, якщо такий перейшов, то switchвиходи. Сорен сказав кілька інших речей, які я хотів сказати, прочитавши цю відповідь.
Тоні Делрой

2
"Будь-який компілятор, котрий вартує своєї солі, буде бачити if / else, якщо сходи і перетворює її в рівноцінний перемикач або навпаки" - будь-яка підтримка цього твердження? компілятор може припустити, що порядок ваших ifпропозицій вже налаштовано на відповідність частоті та відносним потребам у продуктивності, де switchтрадиційно сприймається як відкрите запрошення на оптимізацію, проте компілятор вибере. Хороший момент перескакуючи повз switch:-). Розмір коду залежить від випадків / діапазону - може бути кращим. Нарешті, деякі переліки, бітові поля та charсценарії по суті є дійсними / обмеженими та накладними.
Тоні Делрой

47

До вашого питання:

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  ................

введіть тут опис зображення

Де 00B14538 - вказівник на таблицю стрибків , а значення типу D8 09 AB 00 являє собою вказівник мітки.

2.Чи є цей код за допомогою таблиці стрибків? Ні в цьому випадку.

3.Чому в цьому прикладі немає різниці в продуктивності?

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

4. Чи існує ситуація, в якій є значна різниця у роботі?

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

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

Висновок: Компілятор досить розумний, щоб обробляти подібний випадок і створювати відповідні інструкції :)


(редагувати: nvm, у відповіді Біллі вже є те, що я пропонував. Я думаю, це приємне доповнення.) Було б добре включити gcc -Sвихід: послідовність записів .long L1/ .long L2таблиці є більш значущою, ніж hexdump, і корисніше для когось, що хоче навчитися дивитися на компілятор. (Хоча я думаю, ви просто подивитесь на код перемикання, щоб побачити, чи це був непрямий jmp або купа jcc).
Пітер Кордес

31

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

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

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


13
Я не думаю, що це відповідає на питання ОП. Зовсім.
Біллі ONeal

5
@Soren: Якби це було "основним питанням", то я б не переймався з 179 іншими рядками у запитанні, це був би просто 1 рядок. :-)
користувач541686

8
@Soren: Я бачу щонайменше 3 пронумеровані підпитання як частина запитання ОП. Ви просто трубили таку саму відповідь, яка стосується всіх питань щодо "ефективності", а саме - що вам слід спершу виміряти. Подумайте, що, можливо, Мегрдад уже виміряв і виділив цей фрагмент коду як гарячу точку. У таких випадках ваша відповідь гірша, ніж нікчемна, це шум.
Біллі ONeal

2
Існує розмита лінія між тим, що є таблицею стрибків, і тим, що не залежить від вашого визначення. Я надав інформацію про підпитання, частина 3.
Soren

2
@wnoise: Якщо це єдиний правильний варіант відповіді, то ніколи не буде причин ставити будь-яке запитання про ефективність. Однак у реальному світі є деякі з нас, які вимірюють наше програмне забезпечення, і ми іноді не знаємо, як зробити фрагмент коду швидше, як тільки буде виміряно. Очевидно, що Мегрдад доклав певних зусиль до цього питання, перш ніж задавати його; і я думаю, що його конкретні питання більш ніж відповідальні.
Біллі ONeal

13

Звідки ви знаєте, що ваш комп’ютер не виконував певного завдання, не пов’язаного з тестом, під час тестового циклу комутатора та не виконував менших завдань під час тестового циклу if? Результати тесту не показують нічого, як:

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

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

Я додав:

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

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

Інша проблема з вашим кодом:

switch (counter % 4 + 1)

у циклі перемикання, проти

const size_t c = counter % 4 + 1; 

у вашому циклі if. Дуже велика різниця, якщо ви виправите це. Я вважаю, що розміщення оператора у операторі 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

(подібні результати за кілька спроб)

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


Idk, я не можу цього довести; чи отримуєте ви різні результати?
користувач541686

+1: Бенчмаркінг складний, і ви дійсно не можете зробити жодних висновків із невеликої різниці у часі за один запуск на звичайному комп’ютері. Ви можете спробувати запустити велику кількість тестів і зробити деяку статистику результатів. Або підрахунок циклів процесора на контрольоване виконання в емуляторі.
Томас Падрон-Маккарті

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

1
@BobTurbo: 45983493 старше 12 годин. Це був друкарський помилок?
Гас

1
чудово, тепер мені доведеться знову зробити це :)
BobTurbo

7

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

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

Якщо коротко, якщо комутатор виглядає повільніше, ніж серія ifs, компілятор може просто перетворити його в один. І це, ймовірно, не просто послідовність порівнянь для кожного випадку, а двійкове дерево пошуку. Дивіться тут приклад.


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

5

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


+1 спасибі за відповідь на номер 2! :) (Btw, ось результати з більшою кількістю можливостей.)
користувач541686

4

Я був заінтригований і подивився, що я можу змінити щодо вашого прикладу, щоб змусити його швидше запускати оператор перемикання.

Якщо ви отримаєте до 40, якщо заяви, і додати 0 випадків, то блок if буде працювати повільніше, ніж еквівалентний оператор переключення. У мене є результати тут: https://www.ideone.com/KZeCz .

Ефект від вилучення випадку 0 можна побачити тут: https://www.ideone.com/LFnrX .


1
Ваші посилання зламалися.
TS

4

Ось деякі результати зі старого (тепер важко знайти) стенду ++ орієнтиру:

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 наносекунд. Зі збільшенням кількості тестів час масштабується майже ідеально лінійно.

З твердженням перемикача майже немає різниці в швидкості між двостороннім і 10-ти напрямним тестом, якщо значення щільні. 10-ти напрямний тест із обмеженими значеннями займає приблизно 1,6х стільки ж часу, скільки 10-ти напрямний тест із щільними значеннями - але навіть із розрідженими значеннями, все-таки кращий, ніж удвічі швидкість 10-ти напрямного if/ else if.

Підсумок: використання лише чотиристороннього тесту насправді не покаже вам багато чого щодо продуктивності switchvs if/ else. Якщо ви подивитеся на цифри цього коду, досить просто інтерполювати той факт, що для 4-х напрямного тестування ми очікуємо, що вони дадуть досить схожі результати (~ 2,8 наносекунд для if/ else, ~ 2,0 для switch).


1
Трохи важко знати, що з цього зробити, якщо ми не знаємо, чи тест навмисно шукає значення, яке не відповідає або лише відповідає в кінці if/ elseланцюга проти розсіювання їх тощо. Неможливо знайти bench++джерела після 10 хв.
Тоні Делрой

3

Зауважте, що коли перемикач НЕ компілюється в таблицю стрибків, ви можете дуже часто писати, якщо це більш ефективно, ніж комутатор ...

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

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


Це явно неправда; компілятори більш ніж здатні зробити БОТУ з цих оптимізацій.
Аліса

1
Аліса, як компілятор повинен знати, які випадки трапляться частіше, ніж інші випадки у очікуваному навантаженні? (A: Це можливо не може знати, тому він не може зробити таку оптимізацію.)
Brian Kennedy

(1) це можна зробити легко, і це робиться в деяких компіляторах, просто виконуючи двійковий пошук. (2) можна передбачити різними способами або вказати компілятору. Ви ніколи не використовували "ймовірно" чи "малоймовірно" GCC?
Аліса

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

2

Ні, це, якщо потім стрибати інше, якщо потім стрибати ще ... Таблиця стрибків має таблицю адрес або використовує хеш або щось подібне.

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

Якщо ви використовували випадки 0-3 замість 1-4, компілятор, можливо, використовував таблицю стрибків, компілятор повинен був зрозуміти, як видалити +1. Можливо, це була мала кількість предметів. Якби ви зробили це, наприклад, 0 - 15 або 0 - 31, можливо, він реалізував це за допомогою таблиці або використовував якийсь інший ярлик. Компілятор вільний вибирати, як він реалізує речі, поки відповідає функціональності вихідного коду. І це потрапляє у відмінності компілятора та відмінності версій та оптимізаційні відмінності. Якщо ви хочете таблицю стрибків, зробіть таблицю стрибків, якщо ви хочете, щоб дерево-якщо-то-інше зробить дерево "якщо-то-інше". Якщо ви хочете, щоб компілятор вирішив, використовуйте вимикач / випадок.


2

Не впевнений, чому один швидше, а інший повільніше.

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

У % 20версії перший випадок / якщо завжди той, хто потрапляє. Сучасні процесори "дізнаються", які гілки зазвичай беруться, а які ні, тому вони можуть легко передбачити, як буде вести себе ця гілка майже на кожній ітерації циклу. Це пояснює, чому версія "якщо" летить; він ніколи не повинен виконувати нічого, що минув перший тест, і він (правильно) прогнозує результат цього тесту для більшості ітерацій. Очевидно, що "перемикач" реалізований дещо інакше - можливо, навіть таблиця стрибків, яка може бути повільною завдяки обчисленій гілці.

У % 21версії гілки по суті є випадковими. Тож не тільки багато хто з них виконує кожну ітерацію, процесор не може здогадатися, яким шляхом вони йдуть. Це той випадок, коли таблиця стрибків (або інша оптимізація "переключення"), ймовірно, допоможе.

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

Все, що потрібно сказати, що моє пояснення вище - це багато в чому здогадка. :-)


2
Я не бачу, звідки в сотні разів повільніше може взятися. Найгірший випадок непередбачуваної гілки - це конвеєр конвеєра, який був би в 20 разів повільніше на більшості сучасних процесорів. Не сотні разів. (Гаразд, якщо ви використовуєте старий чіп NetBurst, він може бути на 35 разів повільніше ...)
Біллі ONeal

@Billy: Гаразд, тому я трохи дивлюся вперед. У процесорах Sandy Bridge "Кожна непередбачувана гілка буде промивати весь трубопровід, втрачаючи роботу до ста або близько інструкцій польотів". Трубопроводи дійсно заглиблюються з кожним поколінням, загалом ...
Немо,

1
Неправда. Р4 (NetBurst) мав 31 стадію трубопроводу; Піщаний міст має значно менше етапів. Я думаю, що "втрата роботи 100 або більше інструкцій" є припущенням, що кеш інструкцій стає недійсним. Для загального непрямого стрибка, який насправді відбувається, але для чогось подібного до таблиці стрибків, швидше за все, ціль непрямого стрибка лежить десь у кеш-інструкціях.
Біллі ONeal

@Billy: Я не думаю, що ми не згодні. Моя заява була такою: "Непередбачувані гілки в десятки в сотні разів дорожчі, ніж правильно прогнозовані гілки". Можливо, незначне перебільшення ... Але в глибині I-кешу та глибині конвеєра виконання більше відбувається, ніж просто хіти; з того, що я прочитав, черга лише на декодування становить ~ 20 інструкцій.
Немо

Якщо апаратне забезпечення прогнозування гілок непередбачує шлях виконання, то іполі з неправильного шляху, який знаходиться в конвеєрі інструкцій, просто видаляються там, де вони є, не відкладаючи виконання. Я поняття не маю, як це можливо (чи я неправильнотрактуюце), але, мабуть, вНегалемі немає стояків трубопроводів із непередбачуваними гілками? (Знову ж таки, у мене немає i7; у мене є i5, тому це не стосується мого випадку.)
user541686,

1

Жоден. У більшості випадків, коли ви заходите в асемблер і здійснюєте реальні вимірювання продуктивності, ваше запитання просто неправильне. Для даного прикладу ваше мислення стає остаточно коротким з тих пір

counter += (4 - counter % 4);

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

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