Если оператор против оператора if-else, что быстрее?

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

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

а также:

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

Что, если value такое матрица?

Примечание: я знаю, что value = condition ? 6 : 5; существует, и я ожидаю, что это будет быстрее, но это не вариант.

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

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

79

Решение

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

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

272

Другие решения

Ответ от 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;

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

44

На псевдо-ассемблере,

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

11

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

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

10

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

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 вокруг больших двоичных объектов кода, будь они value = 5 или что-то более сложное. Аналогичным образом, сравнение, даже если это большой блок кода, который все равно должен быть вычислен, и равное или не равное чему-либо, часто компилируется с отрицательным, если (условие) что-то делать часто компилируется, как если бы не условие 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    $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

и компиляторы

с этим я код кода можно ожидать, что различные цели, чтобы соответствовать

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  lrfun0:
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
retfun2:
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 not в коде, в результате чего компилятор выполняет переход if, иначе выполняется через. но это не обязательно из-за стиля кодирования, но из сравнения и случаев if и else в любом синтаксисе.

8

Хорошо, так как ассемблер является одним из тегов, я просто предположу, что ваш код является псевдокодом (и не обязательно 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 в конце).

0
По вопросам рекламы [email protected]