Obie pętle są nieskończone, ale możemy zobaczyć, która z nich pobiera więcej instrukcji / zasobów na iterację.
Korzystając z gcc, skompilowałem dwa następujące programy do montażu na różnych poziomach optymalizacji:
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
Nawet bez optymalizacji ( -O0
) wygenerowany zestaw był identyczny dla obu programów . Dlatego nie ma różnicy prędkości między dwiema pętlami.
Dla odniesienia tutaj jest wygenerowany zespół (przy użyciu gcc main.c -S -masm=intel
flagi optymalizacji):
Z -O0
:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Z -O1
:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Z -O2
i -O3
(ta sama moc wyjściowa):
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.section .text.startup,"x"
.p2align 4,,15
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
W rzeczywistości zespół wygenerowany dla pętli jest identyczny dla każdego poziomu optymalizacji:
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Najważniejsze bity to:
.L2:
jmp .L2
Nie potrafię dobrze czytać asemblera, ale jest to oczywiście bezwarunkowa pętla. jmp
Instrukcja bezwarunkowo resetuje programu tyłu do .L2
etykiety bez nawet porównując wartość przed prawdą, i oczywiście od razu robi to ponownie, aż program jest jakoś skończyło. Odpowiada to bezpośrednio kodowi C / C ++:
L2:
goto L2;
Edytować:
Co ciekawe, nawet bez optymalizacji wszystkie poniższe pętle dały dokładnie taki sam wynik (bezwarunkowy jmp
) w zestawie:
while(42) {}
while(1==1) {}
while(2==2) {}
while(4<7) {}
while(3==3 && 4==4) {}
while(8-9 < 0) {}
while(4.3 * 3e4 >= 2 << 6) {}
while(-0.1 + 02) {}
I nawet ku mojemu zdumieniu:
#include<math.h>
while(sqrt(7)) {}
while(hypot(3,4)) {}
Sprawy stają się trochę bardziej interesujące dzięki funkcjom zdefiniowanym przez użytkownika:
int x(void) {
return 1;
}
while(x()) {}
#include<math.h>
double x(void) {
return sqrt(7);
}
while(x()) {}
Na -O0
te dwa przykłady rzeczywiście wywołać x
i wykonać porównania dla każdej iteracji.
Pierwszy przykład (zwracanie 1):
.L4:
call x
testl %eax, %eax
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Drugi przykład (powrót sqrt(7)
):
.L4:
call x
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jp .L4
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Jednak w -O1
obu przypadkach oba wytwarzają ten sam zestaw co poprzednie przykłady (bezwarunkowy jmp
powrót do poprzedniej etykiety).
TL; DR
W GCC różne pętle są kompilowane do identycznego zestawu. Kompilator ocenia stałe wartości i nie przeszkadza w faktycznym porównaniu.
Morał tej historii jest następujący:
- Istnieje warstwa tłumaczenia między kodem źródłowym C ++ a instrukcjami procesora i ta warstwa ma ważne implikacje dla wydajności.
- Dlatego wydajności nie można ocenić, patrząc tylko na kod źródłowy.
- Kompilator powinien być wystarczająco inteligentny, aby zoptymalizować takie trywialne przypadki. Programiści nie powinni tracić czasu na myślenie o nich w zdecydowanej większości przypadków.