Вопрос:
Я спорил с другом на днях об этих двух фрагментах. Что происходит быстрее и почему?
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 имеет назначение. Переход занимает дополнительное время, а назначение выполняется быстро.