Beide Schleifen sind unendlich, aber wir können sehen, welche mehr Anweisungen/Ressourcen pro Iteration benötigt.
Unter Verwendung von gcc habe ich die beiden folgenden Programme in Assembly mit unterschiedlichen Optimierungsebenen kompiliert:
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
Selbst ohne Optimierungen (-O0
) war das generierte Assembly für beide Programme identisch. Daher gibt es keinen Geschwindigkeitsunterschied zwischen den beiden Schleifen.
Zur Referenz, hier ist das generierte Assembly (mit gcc main.c -S -masm=intel
mit einer Optimierungsoption):
Mit -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"
Mit -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"
Mit -O2
und -O3
(gleiche Ausgabe):
.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"
Tatsächlich ist das für die Schleife generierte Assembly für jede Optimierungsebene identisch:
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Die wichtigen Teile sind:
.L2:
jmp .L2
Ich kann Assembly nicht sehr gut lesen, aber das ist offensichtlich eine bedingungslose Schleife. Die Anweisung jmp
setzt das Programm bedingungslos zurück zum Label .L2
, ohne einen Wert gegen wahr zu vergleichen, und tut dies sofort wieder, bis das Programm auf irgendeine Weise beendet wird. Das entspricht direkt dem C/C++ Code:
L2:
goto L2;
Bearbeitet:
Interessanterweise wurde sogar bei keinen Optimierungen die folgenden Schleifen alle als identische Ausgabe (bedingungsloses jmp
) im Assembly erzeugt:
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) {}
Und sogar zu meiner Verwunderung:
#include
while(sqrt(7)) {}
while(hypot(3,4)) {}
Mit benutzerdefinierten Funktionen wird es etwas interessanter:
int x(void) {
return 1;
}
while(x()) {}
#include
double x(void) {
return sqrt(7);
}
while(x()) {}
Bei -O0
rufen diese beiden Beispiele tatsächlich x
auf und führen für jede Iteration einen Vergleich durch.
Erstes Beispiel (Rückgabe 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"
Zweites Beispiel (Rückgabe 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"
Bei -O1
und höher erzeugen sie jedoch beide dasselbe Assembly wie die vorherigen Beispiele (ein bedingungsloses jmp
zurück zum vorherigen Label).
Zusammenfassung
Unter GCC werden verschiedene Schleifen in identisches Assembly übersetzt. Der Compiler evaluieret die Konstantenwerte und führt keinen tatsächlichen Vergleich durch.
Die Moral von der Geschichte ist:
- Es gibt eine Übersetzungsebene zwischen dem C-Quellcode und den CPU-Anweisungen, und diese Ebene hat wichtige Auswirkungen auf die Leistung.
- Daher kann die Leistung nicht nur durch Betrachten des Quellcodes bewertet werden.
- Der Compiler sollte schlau genug sein, um solche trivialen Fälle zu optimieren. Programmierer sollten in der überwiegenden Mehrheit der Fälle keine Zeit darauf verschwenden, darüber nachzudenken.