Оператор If vs оператор if-else, который быстрее? [закрытый]

я спорил с другом на днях об этих двух фрагментах. Что быстрее и почему ?

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

и:

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

, что если value - это матрица ?

примечание: Я знаю, что и я ожидаю, что это будет быстрее, но это не вариант.

редактировать (запрошено персоналом, так как вопрос на данный момент отложен):

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

6 ответов


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 для типа integer дает 1), потому что ваше намерение может быть не сразу очевидно для людей, читающих ваш код (который может включать ваше будущее "я"). Смысл этого кода заключается в том, чтобы показать, что то, что компилятор производит в обоих случаях (практически) идентично. Чиприан Tomoiaga заявляет Это довольно хорошо в комментариях:

a человека работа заключается в написании кода для человека и пусть компилятор написать код в машина!--20-->.


ответ от CompuChip показывает, что для int они оба оптимизированы для одной и той же сборки, так это не важно.

что делать, если значение является матрицей ?

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

затем

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

неоптимально, потому что в случае condition - это правда, ты ненужная инициализация 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;

но опять же я должен подчеркнуть, что это актуально только тогда, когда строительство и назначения действительно дороги для данного типа. И даже тогда, только профилирования вы знаете наверняка.


на языке псевдо-ассемблера,

    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:

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

  • С любой CPU, произведенный примерно после 1990 года, Хорошая производительность зависит от соответствия кода в кэш команд. Поэтому, если вы сомневаетесь, минимизируйте размер кода. Это весит в пользу первый пример.

  • базовый "в порядке, пятиступенчатый конвейер" CPU, который по-прежнему примерно то, что вы получаете во многих микроконтроллерах, есть пузырь трубопровода каждый раз, когда ветвь-условная или безусловная-берется, поэтому также важно минимизировать количество инструкций ветви. Это также говорит в пользу первого примера.

  • несколько более сложные процессоры-фантазии достаточно, чтобы сделать "для исполнения", но не достаточно фантазии, чтобы использовать наиболее известные реализации этой концепции-может возникнуть пузыри трубопровода всякий раз, когда они сталкиваются писать-после-писать опасности. Это весит в пользу второй пример, где r0 написано только один раз несмотря ни на что. Эти процессоры обычно достаточно причудливы, чтобы обрабатывать безусловные ветви в сборщике инструкций, поэтому вы не просто торгуя write-after-write штраф за ветку штрафа.

    Я не знаю, если кто-то все еще делает этот вид процессора больше. Однако, процессоры,do использование "наиболее известных реализаций" неупорядоченного выполнения, вероятно, сократит углы на менее часто используемых инструкциях, поэтому вам нужно знать, что такого рода вещи могут произойти. Реальный пример ложные зависимости данных от регистров назначения в popcnt и lzcnt на Сэнди Бридж ЦП.

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

    в филиале is очень непредсказуемо, и ваш процессор имеет инструкции условного набора или условного перемещения, пришло время их использовать:

        li    #0, r0
        test  r1
        setne r0
    

    или

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

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


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

Как всегда, если вас это касается, сгенерируйте сборку и посмотрите, что на самом деле делает компилятор.


что заставило бы вас думать, что любой из них даже один лайнер быстрее или медленнее?

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 или что-то более сложное. Аналогично сравнение, даже если это большой blob кода еще нужно вычислить, и равный или не равный чему-то часто компилируется с отрицательным, если (условие) что-то часто компилируется, как если бы не условие goto.

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    ,,
   4:   03e00008    jr  
   8:   24420005    addiu   ,,5

0000000c <fun1>:
   c:   0004102b    sltu    ,,
  10:   03e00008    jr  
  14:   24420005    addiu   ,,5

00000018 <fun2>:
  18:   0004102b    sltu    ,,
  1c:   03e00008    jr  
  20:   24420005    addiu   ,,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 , r0
   e:   1585            mov (sp)+, r5
  10:   0087            rts pc
  12:   15c0 0005       mov , 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 , r0
  28:   1585            mov (sp)+, r5
  2a:   0087            rts pc
  2c:   15c0 0006       mov , 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 , r0
  42:   1585            mov (sp)+, r5
  44:   0087            rts pc
  46:   15c0 0006       mov , 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 кода, и наоборот, ветвь быстрее, чем выполнение через? можно спорить, но казнь должна быть разной. Но это скорее, если условие vs если не условие в коде, в результате чего компилятор делает, если этот переход через else выполняется. но это не обязательно связано со стилем кодирования, но сравнение и случаи if и else в любом синтаксисе.


хорошо, так как сборка является одним из тегов, я просто предположу, что ваш код является псевдо-кодом (и не обязательно c) и перевести его человеком в сборку 6502.

1-й вариант (без else)

        ldy #
        lda #
        dey
        bmi false
        lda #
false   brk

2-й вариант (с else)

        ldy #
        dey
        bmi else
        lda #
        sec
        bcs end
else    lda #
end     brk

предположения: условие в регистре Y установите это в 0 или 1 на первой строке любого варианта, результат будет в аккумуляторе.

Итак, после подсчета циклов для обеих возможностей каждого случая мы видим что 1-я конструкция, как правило, быстрее; 9 циклов, когда условие 0 и 10 циклов, когда условие 1, тогда как вариант два ТАКЖЕ 9 циклов, когда условие 0, но 13 циклов, когда условие 1. (количество циклов не включает BRK В конце).

вывод: If only быстрее If-Else строительство.

и для полноты, вот оптимизированный value = condition + 5 устранение:

ldy #
lda #
tya
adc #
brk

это сокращает наше время до 8 циклы (снова не включая BRK В конце).