Оператор 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
В конце).