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")
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:
ZadaniemA human jest pisanie kodu dla ludzii pozwalanie kompilatorpisać kod dla Maszyny.
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.
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
[[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 wr0
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.popcnt
ilzcnt
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.
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.
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.
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).
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