Чому комутатор не оптимізований так само, як ланцюговий, якщо інший в c / c ++?


39

Наступна реалізація квадрата видає серію операторів cmp / je, як я би сподівався на ланцюжок, якщо заява:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

І далі створюється таблиця даних для повернення:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

Чому gcc не в змозі оптимізувати верхній у нижній?

Розбирання для довідки: https://godbolt.org/z/UP_igi

EDIT: що цікаво, MSVC генерує таблицю стрибків замість таблиці даних для випадку комутатора. І дивно, що Кланг оптимізує їх до того ж результату.


3
Що ви маєте на увазі "невизначена поведінка"? Поки спостережувана поведінка однакова, компілятор може генерувати будь-яку збірку / машинний код, який він хоче
bolov

2
@ user207421 ігнорування returns; корпусів немає breaks, тому комутатор також має певний порядок виконання. У ланцюзі if / else є повернення у кожній гілці, семантика в цьому випадку рівнозначна. Оптимізація не є неможливою . В якості контрприкладу icc не оптимізує жодну з функцій.
користувач1810087

9
Можливо, найпростіша відповідь ... gcc просто не в змозі побачити цю структуру та оптимізувати її (поки що).
користувач1810087

3
Я згоден з @ user1810087. Ви просто знайшли поточну межу процесу вдосконалення компілятора. Підрозділ, який наразі не визнається оптимізованим (деякими компіляторами). Насправді, не кожен інший ланцюг може бути оптимізований таким чином, а лише підмножина, в якій змінна SAME тестується на постійні значення.
Роберто Кабоні

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

Відповіді:


29

Згенерований код для switch-caseзвичайно використовує таблицю стрибків. У цьому випадку пряме повернення через оглядову таблицю представляється оптимізацією, використовуючи той факт, що кожен випадок тут передбачає повернення. Хоча стандарт не дає гарантій на це, я би здивувався, якби компілятор створив серію порівнянь замість таблиці стрибків для звичайного випадку перемикання.

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

Отже, якби я використовував, if-elseтому що я очікую, що більшість дзвінків мають square()бути для інших значень 0або 1рідше для них, то "оптимізація" цього способу для пошуку таблиці насправді може призвести до того, що мій код буде працювати повільніше, ніж я очікував, перемігши мою мету використовувати ifзамість цього a switch. Тож, хоча це дискусійно, я вважаю, що GCC робить все правильно, і Кланг надмірно агресивний у своїй оптимізації.

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

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


2
Так, оптимізація - це меч з різними краями: що вони пишуть, що хочуть, що отримують, і кого ми за це проклинаємо.
Дедуплікатор

1
"... тоді" оптимізація "цього пошуку під час пошуку таблиці насправді призведе до того, що мій код працює повільніше, ніж я очікував ..." Чи можете ви надати виправдання для цього? Чому таблиця стрибків коли-небудь буде повільніше, ніж дві можливі умовні гілки (для перевірки входів на 0та 1)?
Коді Грей

@CodyGray Мені потрібно зізнатися, що я не дійшов до рівня підрахунку циклів - я просто пройшов повне відчуття, що навантаження з пам'яті через покажчик може зайняти більше циклів, ніж порівняння та стрибок, але я можу помилитися. Однак, я сподіваюся, ви погоджуєтесь зі мною, що навіть у цьому випадку, принаймні, для '0', ifочевидно, швидше? Тепер ось приклад платформи, на якій і 0, і 1 були б швидшими при використанні, ifніж при використанні перемикача: godbolt.org/z/wcJhvS (Зверніть увагу, що тут грають багато інших оптимізацій)
th33lf

1
Що ж, підрахунок циклів ніяк не працює в сучасних суперскалярних архітектурах ТОВ. :-) Навантаження з пам'яті не буде повільніше, ніж неправильно передбачувані гілки, тож питання лише наскільки вірогідний передбачити гілку? Це запитання стосується всіх видів умовних гілок, незалежно від того, породжуються вони явними ifзаявами або автоматично компілятором. Я не є експертом з питань зброї, тому я не впевнений, чи твердження, яке ви пред'являєте, щодо switchшвидшого, ніж ifсправжнє. Це залежало б від штрафу за непередбачувані гілки, і це фактично залежало б від того, яка зброя.
Коді Грей

0

Одне можливе обґрунтування полягає в тому, що якщо низькі значення numбільш імовірні, наприклад завжди 0, згенерований код для першого може бути швидшим. Створений код для перемикання займає однаковий час для всіх значень.

Порівнюючи найкращі випадки, згідно з цією таблицею . Дивіться цю відповідь для пояснення таблиці.

Якщо num == 0для "якщо" у вас є xor, test, je (зі стрибком), ret. Затримка: стрибок 1 + 1 +. Однак xor і тест є незалежними, тому фактична швидкість виконання була б швидшою, ніж 1 + 1 циклу.

Якщо num < 7для "перемикання" у вас є mov, cmp, ja (без стрибка), mov, ret. Затримка: 2 + 1 + без стрибка + 2.

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


1
Хм, цікава теорія, але для ifs vs switch у вас є: xor, test, jmp vs mov, cmp jmp. Три інструкції, кожна з останніх - стрибок. Здається, в кращому випадку рівним, ні?
чача15

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