Оператор if vs if-else, который быстрее?
Я спорил с другом на днях об этих двух фрагментах. Что происходит быстрее и почему?
value = 5;
if (condition) {
value = 6;
}
и
if (condition) {
value = 6;
} else {
value = 5;
}
Что, если value
- матрица?
Примечание. Я знаю, что value = condition ? 6 : 5;
существует, и я ожидаю, что он будет быстрее, но это не вариант.
Изменить (запрашивается персоналом, так как вопрос в настоящий момент приостановлен):
- ответьте, рассмотрев либо сборку x86, сгенерированную компиляторами основного потока (например, g++, clang++, vc, mingw) как в оптимизированных, так и в не оптимизированных версиях или в сборке MIPS.
- когда сборка отличается, объясните, почему версия выполняется быстрее и когда (например, "лучше, потому что нет ветвления и ветвления имеет следующий вопрос blahblah" )
Ответы
Ответ 1
TL; DR: В неоптимизированном коде if
без else
кажется неуместным более эффективным, но даже с самым базовым уровнем оптимизации, код в основном переписан на value = condition + 5
.
I дал ему попробовать и сгенерировал сборку для следующего кода:
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
), потому что ваше намерение может быть не сразу очевидным людям, читающим ваш код (который может включать ваше будущее). Точка этого кода должна показать, что то, что производит компилятор в обоих случаях, (практически) идентично. Ciprian Tomoiaga достаточно хорошо говорит в комментариях:
a человеческое задание - написать код для людей и позволить компилятору писать код для машины.
Ответ 2
Ответ от 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);
Но опять же я должен подчеркнуть, что это имеет значение только тогда, когда построение и присвоения действительно дороги для данного типа. И даже тогда, только профилирование, вы точно знаете.
Ответ 3
В неоптимизированном коде первый пример присваивает переменную всегда один раз, а иногда и дважды. Второй пример только присваивает переменную один раз. Условное одно и то же на обоих кодах, так что это не имеет значения. В оптимизированном коде это зависит от компилятора.
Как всегда, если вы заинтересованы, сгенерируйте сборку и посмотрите, что делает на самом деле компилятор.
Ответ 4
В языке псевдоассоциирования
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 года, хорошая производительность зависит от установки кода в кэше инструкций. Поэтому в сомнении минимизируйте размер кода. Это весит в пользу первого примера.
-
С базовым в порядке, пятиступенчатым конвейером "CPU, который по-прежнему примерно совпадает с тем, что вы получаете во многих микроконтроллеров, существует конвейерный пузырь каждый раз, когда выполняется ветвь-условное или безусловное - поэтому важно также минимизировать количество ветвей инструкции. Это также весит в пользу первого примера.
-
Несколько более сложные процессоры - достаточно для того, чтобы сделать " исполнение вне порядка", но недостаточно для использования наиболее известные реализации этой концепции - могут возникать пузырьки трубопровода, когда они сталкиваются с опасностями write-after-write. Это весит в пользу второго примера, где 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
на отправку и выполнение ресурсов; если ветвь на самом деле предсказуема, разветвленная версия может быть быстрее.
Ответ 5
Что заставило бы вас думать, что любой из них даже один лайнер работает быстрее или медленнее?
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 или что-то более сложное. Аналогично сравнению, даже если это большой код кода, он все равно должен быть вычислен и равен или не равен чему-то, часто компилируется с отрицанием, если (условие) что-то часто компилируется, как если бы не условие 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 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 vs, если не условие в коде, в результате чего компилятор делает, если этот переход через else выполняется. но это не обязательно связано с стилем кодирования, но сравнение и случаи if и else в любом синтаксисе.
Ответ 6
Хорошо, поскольку сборка является одним из тегов, я просто предполагаю, что ваш код является псевдокодом (а не обязательно c) и переводит его человеком в сборку 6502.
1-й вариант (без дополнительного)
ldy #$00
lda #$05
dey
bmi false
lda #$06
false brk
Второй вариант (с другим)
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
в конце).
Ответ 7
Оператор if
работает немного быстрее, потому что, когда мы преобразуем этот код в язык ассемблера, if else
выполняет дополнительный скачок, тогда как if
имеет назначение. Переход занимает дополнительное время, а назначение выполняется быстро.