Verificando la optimización

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.

  1. 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.
  2. 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

2 thoughts on “Verificando la optimización

  1. Excelente entrada, gracias por el reporte

    ResponderEliminar
  2. Muy 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

Ever Medina. Con la tecnología de Blogger.