Ambos bucles son infinitos, pero podemos ver cuál toma más instrucciones / recursos por iteración.
Usando gcc, compilé los dos siguientes programas para ensamblar en diferentes niveles de optimización:
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
Incluso sin optimizaciones (-O0
), el ensamblado generado fue idéntico para ambos programas . Por lo tanto, no hay diferencia de velocidad entre los dos bucles.
Como referencia, aquí está el ensamblaje generado (usando gcc main.c -S -masm=intel
con un indicador de optimización):
Con -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"
Con -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"
Con -O2
y-O3
(misma salida):
.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"
De hecho, el ensamblaje generado para el bucle es idéntico para cada nivel de optimización:
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Los bits importantes son:
.L2:
jmp .L2
No puedo leer muy bien el ensamblaje, pero obviamente este es un ciclo incondicional. La jmp
instrucción restablece incondicionalmente el programa a.L2
etiqueta sin siquiera comparar un valor con verdadero, y por supuesto lo vuelve a hacer de inmediato hasta que el programa finalice de alguna manera. Esto corresponde directamente al código C / C ++:
L2:
goto L2;
Editar:
Curiosamente, incluso sin optimizaciones , los siguientes bucles produjeron exactamente el mismo resultado (incondicional jmp
) en el ensamblaje:
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) {}
E incluso para mi asombro:
#include<math.h>
while(sqrt(7)) {}
while(hypot(3,4)) {}
Las cosas se ponen un poco más interesantes con las funciones definidas por el usuario:
int x(void) {
return 1;
}
while(x()) {}
#include<math.h>
double x(void) {
return sqrt(7);
}
while(x()) {}
En -O0
, estos dos ejemplos realmente llamanx
y realizan una comparación para cada iteración.
Primer ejemplo (devolviendo 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"
Segundo ejemplo (regresando 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"
Sin embargo, en -O1
y arriba, ambos producen el mismo ensamblaje que los ejemplos anteriores (un jmp
regreso incondicional a la etiqueta anterior).
TL; DR
Bajo GCC, los diferentes bucles se compilan en un ensamblaje idéntico. El compilador evalúa los valores constantes y no se molesta en realizar ninguna comparación real.
La moraleja de la historia es:
- Existe una capa de traducción entre el código fuente de C ++ y las instrucciones de la CPU, y esta capa tiene implicaciones importantes para el rendimiento.
- Por lo tanto, el rendimiento no puede evaluarse solo mirando el código fuente.
- El compilador debe ser lo suficientemente inteligente como para optimizar estos casos triviales. Los programadores no deberían perder el tiempo pensando en ellos en la gran mayoría de los casos.