Якщо оператор проти оператора if-else, що швидше? [зачинено]


84

Днями я сперечався з другом про ці два фрагменти. Що швидше і чому?

value = 5;
if (condition) {
    value = 6;
}

і:

if (condition) {
    value = 6;
} else {
    value = 5;
}

Що робити, якщо valueце матриця?

Примітка: Я знаю, що value = condition ? 6 : 5;існує, і я сподіваюся, що це буде швидше, але це не був варіант.

Редагувати (запитується співробітниками, оскільки запитання на даний момент утримується):

  • будь ласка, дайте відповідь, розглянувши або збірку x86, створену основними компіляторами ( скажімо, g ++, clang ++, vc, mingw ) як в оптимізованій, так і в неоптимізованій версії, або збірку MIPS .
  • коли збірка відрізняється, поясніть, чому версія швидша і коли ( наприклад, "краще, тому що жодне розгалуження та розгалуження не має наступної проблеми" )

173
Оптимізація знищить все це ... неважливо ...
Квантовий фізик,

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

25
Використання value = condition ? 6 : 5;замість if/else, швидше за все, призведе до створення того самого коду. Подивіться на вихідні дані збірки, якщо ви хочете дізнатись більше.
Jabberwocky

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

11
Єдиний раз, коли має сенс мікрооптимізувати для такої швидкості, всередині циклу, який буде працювати багато разів, і оптимізатор може оптимізувати всі інструкції гілки, як це може зробити gcc для цього тривіального прикладу, або реальний світ продуктивність буде сильно залежати від правильного передбачення гілки (обов’язкове посилання на stackoverflow.com/questions/11227809/… ). Якщо ви неминуче розгалужуватиметесь у циклі, ви можете допомогти предиктору гілок, створивши профіль і перекомпілювавши його.
Девіслор,

Відповіді:


282

TL; DR: У неоптимізованому коді, здається , що ifне elseє доречно більш ефективним, але навіть з увімкненим найосновнішим рівнем оптимізації код в основному переписується value = condition + 5.


Я спробував і створив збірку для наступного коду:

int ifonly(bool condition, int value)
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return value;
}

int ifelse(bool condition, int value)
{
    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return value;
}

У gcc 6.3 з вимкненою оптимізацією ( -O0), відповідна різниця:

 mov     DWORD PTR [rbp-8], 5
 cmp     BYTE PTR [rbp-4], 0
 je      .L2
 mov     DWORD PTR [rbp-8], 6
.L2:
 mov     eax, DWORD PTR [rbp-8]

бо ifonly, поки ifelseмає

 cmp     BYTE PTR [rbp-4], 0
 je      .L5
 mov     DWORD PTR [rbp-8], 6
 jmp     .L6
.L5:
 mov     DWORD PTR [rbp-8], 5
.L6:
 mov     eax, DWORD PTR [rbp-8]

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

Однак навіть при найнижчому рівні оптимізації ( -O1) обидві функції зводиться до однакового:

test    dil, dil
setne   al
movzx   eax, al
add     eax, 5

що в основному еквівалентно

return 5 + condition;

припускаючи, що conditionдорівнює нулю або одиниці. Більш високі рівні оптимізації насправді не змінюють результат, за винятком того, що їм вдається цього уникнути movzx, ефективно обнуляючи EAXрегістр на початку.


Застереження: Вам, мабуть, не слід писати 5 + conditionсамостійно (навіть якщо стандарт гарантує перетворення trueна цілочисельний тип 1), оскільки ваш намір може бути не відразу очевидним для людей, які читають ваш код (що може включати і вашого майбутнього). Суть цього коду полягає в тому, щоб показати, що те, що виробляє компілятор в обох випадках, є (практично) ідентичним. Сіпріан Томояга це досить добре заявляє в коментарях:

Людина Іов, щоб написати код для людей , і нехай компілятор коду для запису машини .


50
Це чудова відповідь, і її слід прийняти.
dtell

10
Я б ніколи не використовував доповнення (<- що робить пітон для тебе.)
Ciprian Tomoiagă,

26
@CiprianTomoiaga, і якщо ви не пишете оптимізатор, ви не повинні! Майже у всіх випадках слід дозволяти компілятору робити такі оптимізації, особливо там, де вони суттєво знижують читабельність коду. Тільки якщо тестування продуктивності виявляє проблеми з певним бітом коду, ви навіть повинні почати намагатися його оптимізувати, і навіть тоді підтримувати його в чистоті та добре коментувати, а також виконувати лише оптимізації, які мають помітну різницю.
Muzer

22
Я хотів відповісти Музеру, але це нічого не додало б до теми. Тим НЕ менше, я просто хочу , щоб знову заявити про те , що Людина Іовом є написання коду для людей , і нехай компілятор коду для запису машини . Я сказав це від розробника компілятора PoV (яким я не є, але трохи дізнався про них)
Ciprian Tomoiagă

10
Значення, trueперетворене на intзавжди, дає 1 крапка. Звичайно, якщо ваш стан просто "істинний", а не boolцінність true, то це вже зовсім інша справа.
TC

44

Відповідь CompuChip показує, що для intних обох оптимізовано одну і ту ж збірку, тому це не має значення.

Що робити, якщо значення є матрицею?

Я буду інтерпретувати це в більш загальному вигляді, тобто що, якщо valueце тип, конструкції та призначення якого дорогі (а переїзди дешеві).

тоді

T value = init1;
if (condition)
   value = init2;

є неоптимальним, оскільки у випадку conditiontrue, ви виконуєте непотрібну ініціалізацію до, init1а потім виконуєте призначення копії.

T value;
if (condition)
   value = init2;
else
   value = init3;

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

У вас є гарне рішення умовного оператора:

T value = condition ? init1 : init2;

Або, якщо вам не подобається умовний оператор, ви можете створити допоміжну функцію наступним чином:

T create(bool condition)
{
  if (condition)
     return {init1};
  else
     return {init2};
}

T value = create(condition);

Залежно від того, що init1і що init2ви також можете врахувати:

auto final_init = condition ? init1 : init2;
T value = final_init;

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


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

@MatthieuM. звичайно. Те, що я мав на увазі під "дорогим", є "дорогим у виконанні (за метрикою, будь то
тактова частота

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

6
@plugwash Розглянемо клас із дуже великим виділеним масивом. Конструктор за замовчуванням виділяє та ініціалізує масив, що є дорогим. Конструктор переміщення (не копіювати!) Може просто поміняти місцями покажчики на вихідний об'єкт, і йому не потрібно виділяти чи ініціалізувати великий масив.
TrentP

1
Поки деталі прості, я б однозначно віддав перевагу використанню ?:оператора перед введенням нової функції. Зрештою, ви, швидше за все, не просто передасте умову функції, а й деякі аргументи конструктора. Деякі з них можуть навіть не використовуватися create()залежно від стану.
cmaster - відновити моніку

12

Мовою псевдоасамблей,

    li    #0, r0
    test  r1
    beq   L1
    li    #1, r0
L1:

може бути, а може і не швидше, ніж

    test  r1
    beq   L1
    li    #1, r0
    bra   L2
L1:
    li    #0, r0
L2:

залежно від того, наскільки складним є фактичний процесор. Перехід від найпростішого до найвигадливішого:

  • З будь-яким процесором, виготовленим приблизно після 1990 року, хороша продуктивність залежить від вмісту коду в кеші інструкцій . Тому, якщо ви сумніваєтесь, мінімізуйте розмір коду. Це важить на користь першого прикладу.

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

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

    Я не знаю, чи хтось все ще робить такий тип процесора. Однак центральні процесори, які використовують «найвідоміші реалізації» позамовного виконання, швидше за все, зменшують кути в менш часто використовуваних інструкціях, тому вам слід знати, що подібні речі можуть трапитися. Реальним прикладом є хибні залежності даних від регістрів призначення в popcntі lzcntна процесорах Sandy Bridge .

  • На найвищому рівні движок OOO закінчить видавати абсолютно однакову послідовність внутрішніх операцій для обох фрагментів коду - це апаратна версія "не хвилюйтеся про це, компілятор згенерує той самий машинний код в будь-якому випадку". Однак розмір коду все ще має значення, і тепер вам також слід турбуватися про передбачуваність умовної гілки. Збої в прогнозуванні відгалужень потенційно можуть спричинити повне промивання трубопроводу , що катастрофічно для продуктивності; див. Чому обробляти відсортований масив швидше, ніж невідсортований? щоб зрозуміти, наскільки це може змінити.

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

        li    #0, r0
        test  r1
        setne r0
    

    або

        li    #0, r0
        li    #1, r2
        test  r1
        movne r2, r0
    

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


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

@supercat Абзац наприкінці призначений для висвітлення цієї справи, але я подумаю, як це зробити зрозумілішим.
zwol

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

Це дуже проникливо
Жюльєн__

9

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

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


1
Якщо вони стурбовані продуктивністю, вони зазвичай не компілюються неоптимізовано. Але, звичайно, наскільки "хорошим" є оптимізатор, залежить від компілятора / версії.
old_timer

AFAIK не коментує, який компілятор / архітектура процесора тощо, тому потенційно їх компілятор не робить оптимізації. Вони можуть компілювати будь-що, починаючи з 8-розрядного PIC і закінчуючи 64-розрядним Xeon.
Ніл

8

Що б змусило вас думати, що хтось із них, навіть один лайнер, швидший чи повільніший?

unsigned int fun0 ( unsigned int condition, unsigned int value )
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return(value);
}
unsigned int fun1 ( unsigned int condition, unsigned int value )
{

    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return(value);
}
unsigned int fun2 ( unsigned int condition, unsigned int value )
{
    value = condition ? 6 : 5;
    return(value);
}

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

00000000 <fun0>:
   0:   e3500000    cmp r0, #0
   4:   03a00005    moveq   r0, #5
   8:   13a00006    movne   r0, #6
   c:   e12fff1e    bx  lr

00000010 <fun1>:
  10:   e3500000    cmp r0, #0
  14:   13a00006    movne   r0, #6
  18:   03a00005    moveq   r0, #5
  1c:   e12fff1e    bx  lr

00000020 <fun2>:
  20:   e3500000    cmp r0, #0
  24:   13a00006    movne   r0, #6
  28:   03a00005    moveq   r0, #5
  2c:   e12fff1e    bx  lr

не велика несподіванка, що вона виконала першу функцію в іншому порядку, однак той самий час виконання.

0000000000000000 <fun0>:
   0:   7100001f    cmp w0, #0x0
   4:   1a9f07e0    cset    w0, ne
   8:   11001400    add w0, w0, #0x5
   c:   d65f03c0    ret

0000000000000010 <fun1>:
  10:   7100001f    cmp w0, #0x0
  14:   1a9f07e0    cset    w0, ne
  18:   11001400    add w0, w0, #0x5
  1c:   d65f03c0    ret

0000000000000020 <fun2>:
  20:   7100001f    cmp w0, #0x0
  24:   1a9f07e0    cset    w0, ne
  28:   11001400    add w0, w0, #0x5
  2c:   d65f03c0    ret

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

Що стосується матриці, не впевнений, що це важливо,

if(condition)
{
 big blob of code a
}
else
{
 big blob of code b
}

просто збираюся розмістити ту саму обгортку if-then-else навколо великих крапок коду, якщо вони мають значення = 5 або щось більш складне. Так само порівняння, навіть якщо це велика крапка коду, його все одно потрібно обчислити, і рівне чи не рівне чомусь часто компілюється з негативним, якщо (умова) робити щось часто компілюється так, ніби не умова гото.

00000000 <fun0>:
   0:   0f 93           tst r15     
   2:   03 24           jz  $+8         ;abs 0xa
   4:   3f 40 06 00     mov #6, r15 ;#0x0006
   8:   30 41           ret         
   a:   3f 40 05 00     mov #5, r15 ;#0x0005
   e:   30 41           ret         

00000010 <fun1>:
  10:   0f 93           tst r15     
  12:   03 20           jnz $+8         ;abs 0x1a
  14:   3f 40 05 00     mov #5, r15 ;#0x0005
  18:   30 41           ret         
  1a:   3f 40 06 00     mov #6, r15 ;#0x0006
  1e:   30 41           ret         

00000020 <fun2>:
  20:   0f 93           tst r15     
  22:   03 20           jnz $+8         ;abs 0x2a
  24:   3f 40 05 00     mov #5, r15 ;#0x0005
  28:   30 41           ret         
  2a:   3f 40 06 00     mov #6, r15 ;#0x0006
  2e:   30 41

ми щойно пройшли цю вправу з кимось іншим на stackoverflow. цікаво, цей компілятор mips у цьому випадку не лише зрозумів, що функції однакові, але одна функція просто перейшла до іншої, щоб заощадити на просторі коду. Не робив цього тут, хоча

00000000 <fun0>:
   0:   0004102b    sltu    $2,$0,$4
   4:   03e00008    jr  $31
   8:   24420005    addiu   $2,$2,5

0000000c <fun1>:
   c:   0004102b    sltu    $2,$0,$4
  10:   03e00008    jr  $31
  14:   24420005    addiu   $2,$2,5

00000018 <fun2>:
  18:   0004102b    sltu    $2,$0,$4
  1c:   03e00008    jr  $31
  20:   24420005    addiu   $2,$2,5

ще деякі цілі.

00000000 <_fun0>:
   0:   1166            mov r5, -(sp)
   2:   1185            mov sp, r5
   4:   0bf5 0004       tst 4(r5)
   8:   0304            beq 12 <_fun0+0x12>
   a:   15c0 0006       mov $6, r0
   e:   1585            mov (sp)+, r5
  10:   0087            rts pc
  12:   15c0 0005       mov $5, r0
  16:   1585            mov (sp)+, r5
  18:   0087            rts pc

0000001a <_fun1>:
  1a:   1166            mov r5, -(sp)
  1c:   1185            mov sp, r5
  1e:   0bf5 0004       tst 4(r5)
  22:   0204            bne 2c <_fun1+0x12>
  24:   15c0 0005       mov $5, r0
  28:   1585            mov (sp)+, r5
  2a:   0087            rts pc
  2c:   15c0 0006       mov $6, r0
  30:   1585            mov (sp)+, r5
  32:   0087            rts pc

00000034 <_fun2>:
  34:   1166            mov r5, -(sp)
  36:   1185            mov sp, r5
  38:   0bf5 0004       tst 4(r5)
  3c:   0204            bne 46 <_fun2+0x12>
  3e:   15c0 0005       mov $5, r0
  42:   1585            mov (sp)+, r5
  44:   0087            rts pc
  46:   15c0 0006       mov $6, r0
  4a:   1585            mov (sp)+, r5
  4c:   0087            rts pc

00000000 <fun0>:
   0:   00a03533            snez    x10,x10
   4:   0515                    addi    x10,x10,5
   6:   8082                    ret

00000008 <fun1>:
   8:   00a03533            snez    x10,x10
   c:   0515                    addi    x10,x10,5
   e:   8082                    ret

00000010 <fun2>:
  10:   00a03533            snez    x10,x10
  14:   0515                    addi    x10,x10,5
  16:   8082                    ret

та компілятори

з цим кодом i можна очікувати, що різні цілі також збігатимуться

define i32 @fun0(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %. = select i1 %1, i32 6, i32 5
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun1(i32 %condition, i32 %value) #0 {
  %1 = icmp eq i32 %condition, 0
  %. = select i1 %1, i32 5, i32 6
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun2(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %2 = select i1 %1, i32 6, i32 5
  ret i32 %2
}


00000000 <fun0>:
   0:   e3a01005    mov r1, #5
   4:   e3500000    cmp r0, #0
   8:   13a01006    movne   r1, #6
   c:   e1a00001    mov r0, r1
  10:   e12fff1e    bx  lr

00000014 <fun1>:
  14:   e3a01006    mov r1, #6
  18:   e3500000    cmp r0, #0
  1c:   03a01005    moveq   r1, #5
  20:   e1a00001    mov r0, r1
  24:   e12fff1e    bx  lr

00000028 <fun2>:
  28:   e3a01005    mov r1, #5
  2c:   e3500000    cmp r0, #0
  30:   13a01006    movne   r1, #6
  34:   e1a00001    mov r0, r1
  38:   e12fff1e    bx  lr


fun0:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB0_2
    mov.w   #5, r15
.LBB0_2:
    pop.w   r4
    ret

fun1:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #5, r15
    cmp.w   #0, r12
    jeq .LBB1_2
    mov.w   #6, r15
.LBB1_2:
    pop.w   r4
    ret


fun2:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB2_2
    mov.w   #5, r15
.LBB2_2:
    pop.w   r4
    ret

Зараз технічно існує різниця в продуктивності деяких з цих рішень, іноді результат 5 - це перехід через результат - 6 код, і навпаки, чи гілка швидша, ніж виконати? можна сперечатися, але виконання має відрізнятися. Але це скоріше умова if проти умови if if у коді, внаслідок чого компілятор виконує if this jump over else виконати через. але це не обов'язково пов'язано зі стилем кодування, а порівнянням та випадками if та else у будь-якому синтаксисі.


0

Гаразд, оскільки збірка - це один із тегів, я просто припущу, що ваш код є псевдокодом (і не обов’язково c), і перекладу його людиною в 6502 збірку.

1-й варіант (без іншого)

        ldy #$00
        lda #$05
        dey
        bmi false
        lda #$06
false   brk

2-й варіант (з іншим)

        ldy #$00
        dey
        bmi else
        lda #$06
        sec
        bcs end
else    lda #$05
end     brk

Припущення: умова в Y-регістрі встановити значення 0 або 1 у першому рядку будь-якого варіанту, результат буде в накопичувачі.

Отже, після підрахунку циклів для обох можливостей кожного випадку ми бачимо, що 1-а конструкція, як правило, швидша; 9 циклів, коли умова дорівнює 0, і 10 циклів, коли умова дорівнює 1, тоді як варіант два - це також 9 циклів, коли умова дорівнює 0, але 13 циклів, коли умова дорівнює 1. ( кількість циклів не включає BRKв кінці ).

Висновок: If onlyшвидше, ніж If-Elseпобудувати.

І для повноти, ось оптимізоване value = condition + 5рішення:

ldy #$00
lda #$00
tya
adc #$05
brk

Це скорочує наш час до 8 циклів ( знову не враховуючи BRKкінець ).


6
На жаль, на цю відповідь, подача одного і того ж вихідного коду в компілятор C (або в компілятор C ++) дає значно інші результати, ніж подача його в мозок Глена. Немає різниці, жодного потенціалу "оптимізації" між будь-якими альтернативами на рівні вихідного коду. Просто використовуйте той, який є найбільш читабельним (імовірно, якщо / інший).
Quuxplusone

1
@ Так. Компілятор може або оптимізувати обидва варіанти до найшвидшої версії, або, ймовірно, додати додаткові накладні витрати, що значно перевищує різницю між ними. Або обидва.
jpaugh

1
Припускаючи, що це " не обов'язково C ", здається розумним вибором, враховуючи те, що питання позначено як C ++ (на жаль, йому не вдається оголосити типи змінних, що беруть участь).
Toby Speight
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.