If statement vs if-else statement, które jest szybsze? [zamknięte]

Pokłóciłem się kiedyś z przyjacielem o te dwa fragmenty. Co jest szybsze i dlaczego ?

value = 5;
if (condition) {
    value = 6;
}

I:

if (condition) {
    value = 6;
} else {
    value = 5;
}

Co jeśli value jest macierzą ?

Uwaga: wiem, że value = condition ? 6 : 5; Istnieje i oczekuję, że będzie szybciej, ale nie było takiej opcji.

Edit (wymagane przez personel, ponieważ pytanie jest w tej chwili wstrzymane):

  • proszę odpowiedzieć rozważając x86 assembly wygenerowane przez kompilatory głównego nurtu (powiedzmy g++, clang++, vc, MinGW) w wersji zoptymalizowanej i nie zoptymalizowanej lub MIPS assembly.
  • gdy assembly różnią się, wyjaśnij dlaczego wersja jest szybsza i kiedy ( np. "lepiej bo bez rozgałęzień i rozgałęzień ma następujący problem blahblah")
Author: Julien__, 2017-04-04

6 answers

TL; DR: w kodzie nieoptimizowanym, if bez else wydaje się mało wydajny, ale przy włączonym nawet najbardziej podstawowym poziomie optymalizacji kod jest w zasadzie przepisywany na value = condition + 5.


I spróbowałem i wygenerowałem zestaw dla następującego kodu:

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;
}

W gcc 6.3 z wyłączonymi optymalizacjami (-O0), istotna różnica wynosi:

 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]

Dla ifonly, natomiast ifelse ma

 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]
[[18]} ten ostatni wygląda nieco mniej wydajny, ponieważ ma dodatkowy skok, ale oba mają co najmniej dwa i co najwyżej trzy zadania, więc chyba, że naprawdę musisz wycisnąć każdą ostatnią kroplę wydajności (wskazówka: jeśli nie pracujesz na promie kosmicznym, a nawet wtedy {32]}prawdopodobnie {33]} Nie) różnica nie będzie zauważalna. Jednak nawet przy najniższym poziomie optymalizacji (-O1) obie funkcje redukują się do tego samego:
test    dil, dil
setne   al
movzx   eax, al
add     eax, 5

Który jest w zasadzie odpowiednikiem

return 5 + condition;

Zakładając condition jest zerem lub jedynką. Wyższe poziomy optymalizacji tak naprawdę nie zmieniają wyjścia, z wyjątkiem tego, że udaje im się uniknąć movzx poprzez efektywne zerowanie rejestru EAX na początku.


Zastrzeżenie: prawdopodobnie nie powinieneś pisać 5 + condition samemu (mimo że standard gwarantuje, że konwersja {[16] } na typ integer daje 1), ponieważ twoje intencje mogą nie być natychmiast oczywiste dla osób czytających Twój kod (który może zawierać Twoje przyszłe ja). The point of kod ten ma pokazać, że to co kompilator produkuje w obu przypadkach jest (praktycznie) identyczne. Ciprian Tomoiaga stwierdza to całkiem dobrze w komentarzach:

Zadaniem

A human jest pisanie kodu dla ludzii pozwalanie kompilatorpisać kod dla Maszyny.

 271
Author: CompuChip,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:20

Odpowiedź z CompuChip pokazuje, że dla int oba są zoptymalizowane do tego samego zestawu, więc nie ma to znaczenia.

Co jeśli wartość jest macierzą ?

Zinterpretuję to w bardziej ogólny sposób, tzn. co jeśli {[6] } jest typu, którego konstrukcje i zadania są drogie (a ruchy są tanie).

Then

T value = init1;
if (condition)
   value = init2;

Jest nieoptymalne, ponieważ w przypadku, gdy condition jest prawdziwe, niepotrzebnie inicjalizujesz init1, a następnie wykonaj zadanie kopiowania.

T value;
if (condition)
   value = init2;
else
   value = init3;
Tak jest lepiej. Ale nadal nieoptymalne, jeśli budowa domyślna jest droga i jeśli budowa kopiowania jest droższa, to inicjalizacja.

Masz rozwiązanie operatora warunkowego, które jest dobre:

T value = condition ? init1 : init2;

Lub, jeśli nie podoba Ci się operator warunkowy, możesz utworzyć funkcję pomocniczą w następujący sposób:

T create(bool condition)
{
  if (condition)
     return {init1};
  else
     return {init2};
}

T value = create(condition);

W zależności od tego, co init1 i init2 są Możesz również rozważyć to:

auto final_init = condition ? init1 : init2;
T value = final_init;

Ale znowu muszę podkreślić, że jest to istotne tylko wtedy, gdy budowa i zadania są naprawdę drogie dla danego typu. I nawet wtedy, tylko przez profilowanie wiesz na pewno.

 44
Author: bolov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-07 15:11:03

W języku pseudo-assembly,

    li    #0, r0
    test  r1
    beq   L1
    li    #1, r0
L1:

może być szybszy od

    test  r1
    beq   L1
    li    #1, r0
    bra   L2
L1:
    li    #0, r0
L2:

W zależności od tego, jak zaawansowany jest procesor. Od najprostszych do najbardziej fantazyjnych:

  • W przypadku dowolnego procesora wyprodukowanego po około 1990 roku, dobra wydajność zależy od dopasowania kodu w pamięci podręcznej instrukcji. W razie wątpliwości Minimalizuj rozmiar kodu. To na korzyść pierwszego przykładu.

  • Z podstawowym "W porządku, pięciostopniowy rurociąg " procesor, który nadal jest mniej więcej tym, co dostajemy w wielu mikrokontrolerach, za każdym razem, gdy Branch jest Branch-warunkowa lub bezwarunkowa-jest Branch bubble . To również jest na korzyść pierwszego przykładu.

  • Nieco bardziej wyrafinowane Procesory - wystarczająco fantazyjne, aby wykonać "out-of-order execution", ale za mało fantazyjne, aby użyć najbardziej znanych implementacje tego pojęcia-mogą wywoływać bańki rurociągów, gdy napotykają zagrożenia związane z zapisem po zapisie. Na korzyść przykładu second , gdzie r0 jest napisane tylko raz, bez względu na wszystko. Procesory te są zwykle na tyle fantazyjne, że mogą przetwarzać bezwarunkowe gałęzie w pliku pobierania Instrukcji, więc nie są tylko wymieniają karę zapisu po zapisie na karę gałęzi.

    [[8]] Nie wiem, czy ktoś jeszcze produkuje tego typu procesory. Jednak procesory, które wykonują używają "najbardziej znanych implementacji" out-of-order execution, prawdopodobnie skrócą czas wykonywania rzadziej używanych instrukcji, więc musisz mieć świadomość, że tego typu rzeczy mogą się zdarzyć. Prawdziwym przykładem są fałszywe zależności danych od rejestrów docelowych w popcnt i lzcnt na procesorach Sandy Bridge .
  • Na najwyższym końcu, silnik OOO skończy wydając dokładnie taką samą sekwencję operacji wewnętrznych dla obu kodów fragmenty-jest to wersja sprzętowa " nie martw się o to, kompilator wygeneruje ten sam kod maszynowy tak czy inaczej."Jednak rozmiar kodu nadal ma znaczenie, a teraz również powinieneś martwić się przewidywalnością gałęzi warunkowej. Przewidywanie gałęzi awarie potencjalnie powodują całkowity rurociąg flush, co jest katastrofalne dla wydajności; zobacz dlaczego szybsze jest przetwarzanie posortowanej tablicy niż niesortowanej tablicy? zrozumieć, ile to może zrobić różnicę.

    Jeśli gałąź jest wysoce nieprzewidywalna, a Twój procesor ma instrukcje warunkowe-set lub warunkowe-move, jest to czas na ich użycie:

        li    #0, r0
        test  r1
        setne r0
    

    Lub

        li    #0, r0
        li    #1, r2
        test  r1
        movne r2, r0
    

    Wersja warunkowa jest również bardziej zwarta niż jakakolwiek inna alternatywa; jeśli ta instrukcja jest dostępna, to jest praktycznie gwarantowana, że będzie odpowiednia dla tego scenariusza, nawet jeśli gałąź była przewidywalna. Wersja warunkowo-ruchowa wymaga dodatkowego scratch rejestruje i zawsze marnuje jedną instrukcję li dotyczącą wysyłania i uruchamiania zasobów; jeśli gałąź była w rzeczywistości przewidywalna, rozgałęziona wersja może być szybsza.

 11
Author: zwol,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-16 15:42:18

W kodzie nieoptymalizowanym, pierwszy przykład przypisuje zmienną zawsze raz, a czasami dwa razy. Drugi przykład przypisuje zmienną tylko raz. Warunek warunkowy jest taki sam na obu ścieżkach kodu, więc nie powinno to mieć znaczenia. W zoptymalizowanym kodzie zależy to od kompilatora.

Jak zawsze, jeśli jesteś tak zaniepokojony, Wygeneruj assembly i zobacz, co kompilator faktycznie robi.

 10
Author: Neil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-04 08:31:19

Dlaczego myślisz, że każdy z nich, nawet jeden liner jest szybszy lub wolniejszy?

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);
}

Więcej linii kodu języka wysokiego poziomu daje kompilatorowi więcej do pracy, więc jeśli chcesz wprowadzić ogólną zasadę, daj kompilatorowi więcej kodu do pracy. Jeśli algorytm jest taki sam jak w powyższych przypadkach, to można oczekiwać, że kompilator z minimalną optymalizacją to rozgryzie.

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

Nic dziwnego, że pierwsza funkcja była w innej kolejności, ta sama czas wykonania.

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

Mam nadzieję, że wpadłeś na pomysł, że mogłeś po prostu spróbować, jeśli nie było oczywiste, że różne implementacje nie były w rzeczywistości różne.

Jeśli chodzi o matrycę, Nie wiem, jakie to ma znaczenie,

if(condition)
{
 big blob of code a
}
else
{
 big blob of code b
}

Po prostu umieszczę to samo if-then-else wokół dużych blobów kodu, czy to wartość = 5, czy coś bardziej skomplikowanego. Podobnie porównanie, nawet jeśli jest to duża plama kodu, nadal musi być obliczone i równe lub nie equal to something jest często kompilowany z ujemnym, if (warunek) do something jest często kompilowany tak, jakby nie warunek 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

Właśnie przechodziliśmy przez to ćwiczenie z kimś innym niedawno na stackoverflow. ten kompilator MIPS co ciekawe w tym przypadku nie tylko zdawał sobie sprawę, że funkcje są takie same, ale miał jedną funkcję po prostu przeskoczyć do drugiej, aby zaoszczędzić miejsce na kodzie. Nie zrobiłem tego tutaj

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

Jeszcze kilka celów.

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

I Kompilatory

Z tym kodem i można by oczekiwać, że różne cele również pasują

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

Teraz technicznie jest różnica wydajności w niektórych z tych rozwiązań, czasami wynikiem jest 5 przypadek ma skok nad wynikiem jest 6 kod i odwrotnie, czy gałąź jest szybsza niż wykonanie przez? można się spierać, ale egzekucja powinna być różna. Ale jest to bardziej warunek if vs warunek if not w kodzie, co powoduje, że kompilator wykonuje if this jump over else wykonać. ale nie musi to być spowodowane stylem kodowania, ale porównaniem i przypadkami if I else w dowolnej składni.

 8
Author: old_timer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-04 18:27:58

OK, ponieważ assembly jest jednym ze znaczników, po prostu zakładam, że Twój kod jest pseudo kodem (i niekoniecznie c) i tłumaczę go przez człowieka na 6502 assembly.

Pierwsza opcja (bez innych)

        ldy #$00
        lda #$05
        dey
        bmi false
        lda #$06
false   brk

Druga opcja (z else)

        ldy #$00
        dey
        bmi else
        lda #$06
        sec
        bcs end
else    lda #$05
end     brk

Założenia: warunek jest w rejestrze Y ustaw to na 0 LUB 1 w pierwszej linii obu opcji, wynik będzie w akumulatorze.

Tak więc, po zliczeniu cykli dla obu możliwości każdego przypadku, widzimy, że 1. konstrukcja jest ogólnie szybciej; 9 cykli, gdy warunek jest 0 i 10 cykli, gdy warunek jest 1, podczas gdy opcja druga jest również 9 cykli, gdy warunek jest 0, ale 13 cykli, gdy warunek jest 1. (liczba cykli nie obejmuje BRK na końcu ).

Wniosek: If only jest szybszy niż If-Else konstruować.

I dla kompletności, tutaj jest zoptymalizowane value = condition + 5 rozwiązanie:

ldy #$00
lda #$00
tya
adc #$05
brk

To skraca nasz czas do 8 cykli ( ponownie nie wliczając BRK na końcu).

 0
Author: Glen Yates,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-06 13:04:10