Подтвердить что ты не робот

Оператор 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" )
4b9b3361

Ответ 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 имеет назначение. Переход занимает дополнительное время, а назначение выполняется быстро.