Я спорил с другом на днях об этих двух фрагментах. Что быстрее и почему?
value = 5;
if (condition) {
value = 6;
}
а также:
if (condition) {
value = 6;
} else {
value = 5;
}
Что, если value
такое матрица?
Примечание: я знаю, что value = condition ? 6 : 5;
существует, и я ожидаю, что это будет быстрее, но это не вариант.
редактировать (запрошено персоналом, поскольку вопрос на данный момент приостановлен):
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
) потому что ваше намерение может быть не сразу очевидным для людей, читающих ваш код (который может включать ваше будущее я). Смысл этого кода в том, чтобы показать, что то, что производит компилятор в обоих случаях, (практически) идентично. Киприан Томойага утверждает это довольно хорошо в комментариях:
человекработа заключается в написании кода для людей и пусть компилятор написать код для машина.
Ответ от 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:
в зависимости от того, насколько сложен фактический процессор. Переходя от самого простого к причудливому:
С любой Процессор, выпущенный примерно после 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
ценность инструкции по отправке и выполнению ресурсов; если ветвление было на самом деле предсказуемым, ветвистая версия вполне может быть быстрее.
В неоптимизированном коде первый пример назначает переменную всегда один раз, а иногда и дважды. Во втором примере переменная назначается только один раз. Условие одинаково для обоих путей кода, поэтому это не должно иметь значения. В оптимизированном коде это зависит от компилятора.
Как всегда, если вас это беспокоит, сгенерируйте сборку и посмотрите, что на самом деле делает компилятор.
Что заставило бы вас думать, что любой из них, даже один лайнер, быстрее или медленнее?
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 в любом синтаксисе.
Хорошо, так как ассемблер является одним из тегов, я просто предположу, что ваш код является псевдокодом (и не обязательно 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
в конце).