optimizacion - ¿Cómo GCC optimiza una variable no utilizada incrementada dentro de un bucle?
optimizacion de codigo en c++ (2)
Escribí este sencillo programa en C:
int main() {
int i;
int count = 0;
for(i = 0; i < 2000000000; i++){
count = count + 1;
}
}
Quería ver cómo el compilador gcc optimiza este ciclo (claramente agregue 1 2000000000 veces debería ser "agregar 2000000000 una vez"). Asi que:
gcc test.c y luego time
on a.out
da:
real 0m7.717s
user 0m7.710s
sys 0m0.000s
$ gcc -O2 test.c y luego time on
a.out` da:
real 0m0.003s
user 0m0.000s
sys 0m0.000s
Luego desarmé ambos con gcc -S
. El primero parece bastante claro:
.file "test.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
movl $0, -8(%rbp)
movl $0, -4(%rbp)
jmp .L2
.L3:
addl $1, -8(%rbp)
addl $1, -4(%rbp)
.L2:
cmpl $1999999999, -4(%rbp)
jle .L3
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits
L3 agrega, L2 compara -4(%rbp)
con 1999999999
y loops a L3 si i < 2000000000
.
Ahora el optimizado:
.file "test.c"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
rep
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits
No puedo entender en absoluto lo que está pasando allí! Tengo poco conocimiento de ensamblaje, pero esperaba algo así como
addl $2000000000, -8(%rbp)
Incluso probé con gcc -c -g -Wa, -a, -ad -O2 test.c para ver el código C junto con el conjunto al que se convirtió, pero el resultado no fue más claro que el anterior.
¿Puede alguien explicar brevemente:
- La salida gcc -S-O2 .
- Si el ciclo está optimizado como esperaba (una suma en lugar de muchas sumas)?
El compilador es incluso más inteligente que eso. :)
De hecho, se da cuenta de que no está utilizando el resultado del ciclo. ¡Así que sacó todo el ciclo por completo!
Esto se llama eliminación del código muerto .
Una mejor prueba es imprimir el resultado:
#include <stdio.h>
int main(void) {
int i; int count = 0;
for(i = 0; i < 2000000000; i++){
count = count + 1;
}
// Print result to prevent Dead Code Elimination
printf("%d/n", count);
}
EDITAR: He agregado el #include <stdio.h>
requerido #include <stdio.h>
; el listado del ensamblado de MSVC corresponde a una versión sin el #include
, pero debería ser el mismo.
No tengo GCC delante de mí en este momento, ya que estoy iniciado en Windows. Pero aquí está el desmontaje de la versión con printf()
en MSVC:
EDITAR: tuve una salida de ensamblaje incorrecta. Esta es la correcta.
; 57 : int main(){
$LN8:
sub rsp, 40 ; 00000028H
; 58 :
; 59 :
; 60 : int i; int count = 0;
; 61 : for(i = 0; i < 2000000000; i++){
; 62 : count = count + 1;
; 63 : }
; 64 :
; 65 : // Print result to prevent Dead Code Elimination
; 66 : printf("%d/n",count);
lea rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6?$AA@
mov edx, 2000000000 ; 77359400H
call QWORD PTR __imp_printf
; 67 :
; 68 :
; 69 :
; 70 :
; 71 : return 0;
xor eax, eax
; 72 : }
add rsp, 40 ; 00000028H
ret 0
Entonces sí, Visual Studio hace esta optimización. Supongo que GCC probablemente también lo haga.
Y sí, GCC realiza una optimización similar. Aquí hay una lista de ensamblaje para el mismo programa con gcc -S -O2 test.c
(gcc 4.5.2, Ubuntu 11.10, x86):
.file "test.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d/n"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $2000000000, 8(%esp)
movl $.LC0, 4(%esp)
movl $1, (%esp)
call __printf_chk
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits
Los compiladores tienen algunas herramientas a su disposición para hacer que el código sea más eficiente o más "eficiente":
Si el resultado de un cálculo nunca se utiliza, el código que realiza el cálculo se puede omitir (si el cálculo actuó sobre valores
volatile
, esos valores todavía se deben leer, pero los resultados de la lectura se pueden ignorar). Si los resultados de los cálculos que lo alimentaron no se usaron, también se puede omitir el código que los realiza. Si tal omisión hace que el código para ambas rutas en una rama condicional sea idéntico, la condición puede considerarse como no utilizada y omitida. Esto no tendrá ningún efecto sobre los comportamientos (que no sean el tiempo de ejecución) de ningún programa que no haga accesos de memoria fuera de límites ni invoque lo que el Anexo L llamaría "Comportamientos no definidos críticos".Si el compilador determina que el código de máquina que calcula un valor solo puede producir resultados en un cierto rango, puede omitir cualquier prueba condicional cuyo resultado pueda predecirse sobre esa base. Como se indicó anteriormente, esto no afectará los comportamientos que no sean el tiempo de ejecución a menos que el código invoque "Comportamientos no definidos críticos".
Si el compilador determina que ciertas entradas invocarían cualquier forma de Comportamiento Indefinido con el código tal como está escrito, el Estándar permitiría al compilador omitir cualquier código que solo sería relevante cuando se reciban tales entradas, incluso si el comportamiento natural de la plataforma de ejecución dado que tales entradas habrían sido benignas y la reescritura del compilador lo haría peligroso.
Los buenos compiladores hacen # 1 y # 2. Por alguna razón, sin embargo, # 3 se ha puesto de moda.