Seré breve y solo les comentaré sobre una de las maneras de probar si el código realmente se está optimizando.
Utilizando el comando time que forma parte de los sistemas Unix-like podemos conseguir el tiempo que tarda en ejecutarse nuestro programa.
Esto incluye el tiempo real de computo( tiempo total de ejecucción ), el tiempo de usuario ( es el tiempo que tarda en los calculos y las llamadas al kernel ) y el tiempo de uso del sistema ( solo las llamadas al kernel ).
Podemos decir que cuando un programa esta haciendo iteraciones se esta acumulando tiempo de usuario solamente.
El resultado se nos despliega con
0m0.000s
minutos
segundos
milisegundos
El manual nos dice que el uso de este comando es de la siguiente forma:
time [options] command [arguments...]
para este caso solo nos interesan los tiempos así que no le daremos opciones.
Ejemplo:
int main(int argi,char *argv[]){ argi = 0; puts("Inicio"); while(!(argi == 1000000)){ puts("Love,Love,Love"); argi++; } puts("final"); return 0; }
usaremos este codigo que imprime 1 millón de veces una cadena de caracteres.
Lo compararemos con este código que hace lo mismo, pero escrito en assembly con sintaxis de Intel.
section .data hello: db 'Inicio',10,13 slong: equ $-hello notDone: db 'Love, Love, Love',10,13 notlong: equ $-notDone done: db 'final',10,13 donelong: equ $-done section .text global main main: mov eax,4 mov ebx,1 mov ecx,hello mov edx,slong int 0x80 xor ecx,ecx loop: push ecx mov eax,4 mov ebx,1 mov ecx,notDone mov edx,notlong int 0x80 pop ecx add ecx,1 cmp ecx,1000000 ;cantidad de ciclos jl loop theend: mov eax,4 mov ebx,1 mov ecx,done mov edx,donelong int 0x80
Utilizamos el comando time de la siguiente manera:
*mande la salida de datos hacia /dev/null
para evitar ver el millon de impresiones en pantalla*
time ./cicloAssembly > /dev/null
time ./cicloC > /dev/null
Es obvio cual es mas eficiente, ¿no?.
Esto sucede porque el codigo en assembly hace una system call en cada ciclo, es decir realiza un millón de llamadas al kernel. Sumando ese millón de llamadas al kernel a las veces que ese proceso sufrio de un switch context, page faults o fue manipulado por el scheduler nos da un tiempo relativamente largo.
El ejecutable de C es mucho muy rapido por el simple hecho de no hacer llamadas al kernel.
Ahora usare el comando gcc -S ciclo.c y eliminare todas las lineas del GDB para despues compilarlo y apreciar si eliminando las lineas de debuggeo se logra optimizar un poco el ejecutable.
El codigo assembly quedo asi ( sin lineas de debuggeo )
.LC0: .string "Inicio" .LC1: .string "Love,Love,Love" .LC2: .string "final" .text .globl main .type main, @function main: .LFB0: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $32, %esp movl $0, 28(%esp) movl $.LC0, (%esp) call puts jmp .L2 .L3: movl $.LC1, (%esp) call puts addl $1, 28(%esp) .L2: cmpl $1000000, 28(%esp) jne .L3 movl $.LC2, (%esp) call puts movl $0, %eax leave ret
time ./cicloCtoASM > /dev/null
Ahora vemos 2 cosas interesantes.
- El tiempo de usuario no cambio, es decir que para dicho programa queda claro que el tiempo que tarda en imprimir un millón de veces la cadena de caracteres es de 77 milisegundos y el tiempo real solo difiere de 1 milisegundo lo cual puede ser causa del scheduler.
- A pesar de ser un programa en assembly no se realizaron llamadas al kernel, es decir que como podemos ver en el codigo, solo se estan llamando las funciones del lenguaje C.
Con estas pruebas podemos darnos cuenta que el codigo assembly escrito desde scratch puede llegar a ser muy pesado y no tan eficiente en terminos de velocidad de ejecucción mas sin embargo es mas versatil porque puede utilizar alrededor de 190 system calls diferentes, ademas que se puede ver que el hecho de traducir el codigo de C hacia Assembly no quiere decir que este será reducido al nivel mas bajo de integración con el procesador.
Por mi parte mi tarea sobre lenguaje ensamblador no fue en pro de una optimización, fue dirigida hacia el uso de las system calls, pero para aquellos que buscaban optimización esta entrada puede ser útil.
Como recomendación: si quieren velocidad utilizen el puts() ya que es mucho mas rapido que el printf. Verifiquen el codigo.s si le llegan a poner mas de 1 millón de iteraciones en su codigo de C, por alguna extraña razón el codigo.s quedaba sin delimitantes haciendo un loop infinito, y también si llegan a usar un trillón de iteraciones no duden en irse por un café true story.
Referencia:
printf versus puts
Referencia:
printf versus puts
Excelente entrada, gracias por el reporte
ResponderEliminarMuy bien; van 8 para el lab de integrados. En la oficina tengo un excelente libro sobre compiladores que discute en profundidad aspectos de optimización de código, por si quieres que te lo preste.
ResponderEliminar