viernes, 17 de febrero de 2012

for vs while

Hoy quiero mostrar una pequeña diferencia entre el for y el while. Para ello vamos a empezar con el siguiente fragmento de código.

char	*cptr;
char	*texto = "abcdefghijklmnopq";
for (cptr=texto;*cptr;cptr++)
{
    printf("%c",*cptr);
}
cptr = texto;
while (*cptr)
{
   printf("%c",*cptr);
   cptr++;
}

Utilizando el compilador de Microsoft sin optimizaciones obtenemos.

for (cptr=texto;*cptr;cptr++)

  mov eax,dword ptr [ebp-14h]
  mov dword ptr [ebp-10h],eax
  jmp 1
2:mov ecx,dword ptr [ebp-10h]
  add ecx,1
  mov dword ptr [ebp-10h],ecx
1:mov edx,dword ptr [ebp-10h]
  movsx eax,byte ptr [edx]
  test eax,eax
  je 3
cptr = texto;
while (*cptr)
3:mov eax,dword ptr [ebp-14h]
  mov dword ptr [ebp-10h],eax
  
  
  
  
  mov ecx,dword ptr [ebp-10h]
  movsx edx,byte ptr [ecx]
  test edx,edx
  je 4
printf("%c",*cptr);
  mov ecx,dword ptr [ebp-10h]
  movsx edx,byte ptr [ecx]
  push edx
  push offset string "%c"
  call printf
  add esp,8
  jmp 2
printf("%c",*cptr);
  mov eax,dword ptr [ebp-10h]
  movsx ecx,byte ptr [eax]
  push ecx
  push offset string "%c"
  call printf
  add  esp,8
cptr++;
  mov edx,dword ptr [ebp-10h]
  add edx,1
  mov dword ptr [ebp-10h],edx
  jmp 3
4:

Como se aprecia utilizando el while hemos eliminado la instruccion jmp 1 el resto del código se mantiene inalterado. Este comportamiento que acabamos de observar se debe a que el ciclo for genera las instrucciones de lazo al comienzo.

Veamos que ocurre si necesitamos ignorar el tratamiento del caracter b, es decir este no sera mostrado por pantalla.

for (cptr=texto;*cptr;cptr++)
{
    if (*cptr =='b') continue;
    printf("%c",*cptr);
}
cptr = texto;
while (*cptr)
{
   if (*cptr != 'b')
   {
      printf("%c",*cptr);
   }
   cptr++;
}

Como podemos apreciar el uso del for genera un código mas limpio, ya que utilizando la sentencia continue se puede ir a la proxima iteración, mientras que en un while hay que invertir la condicion para poder ejecutar la instruccion de iteracion como si de un else se tratara.

for (cptr=texto;*cptr;cptr++)
 ... 
if (*cptr =='b') continue;
  mov ecx,dword ptr [ebp-10h]
  movsx edx,byte ptr [ecx]
  cmp   edx,62h
  jne   5
  jmp   2
printf("%c",*cptr);
5: ..
while (*cptr)
 ... 
if (*cptr != 'b')
  mov edx,dword ptr [ebp-10h]
  movsx eax,byte ptr [edx]
  cmp eax,62h
  je  6

cptr++;
6: ...

No me explico porque el compilador utilizo dos instrucciones de salto en el caso del for, de no haber sido asi los dos ejemplos serían idénticos. A modo de resumen utilizando el while hemos conseguido reducir una instruccion, si utilizamos la sentencia continue se genera la misma cantidad de instrucciones en los dos casos (o debería), eso si el codigo con while se vuelve mas extenso visualmente

En estos ejemplos sencillos donde se esta iterando una cadena de caracteres se pueden eliminar un par de instrucciones más si almacenamos el valor del caracter en una variable temporal.

char	c;
cptr = texto;
while (*cptr)
{
   c = *cptr;
   if (c != 'b')
   {
      printf("%c",c);
   }
   cptr++
}




if (*cptr !='b')
  mov edx,dword ptr [ebp-10h]
  movsx eax,byte ptr [edx]
  cmp eax,62h
  je  6
printf("%c",*cptr);
  mov ecx,dword ptr [ebp-10h]
  movsx edx,byte ptr [ecx]
  push edx
  push offset string "%c"
  call printf
  add esp,8
  jmp 2
c = *cptr;
  mov ecx,dword ptr [ebp-10h]
  mov dl,byte ptr [ecx]
  mov byte ptr [ebp-14h],dl
if (c != 'b')
  movsx edx,byte ptr [ebp-14h]
  cmp edx,62h
  je 6

printf("%c",c);
  movsx ecx,byte ptr [ebp-14h]
  push ecx
  push offset string "%c"
  call printf 
  add esp,8
  jmp 2

A pesar de utilizar 3 instrucciones para inicializar la variable c , cada vez que accedemos a *cptr nos ahorramos una instrucción, en tres accesos amortizamos el coste. Quiero aclarar que esto no es aplicable cuando iteramos entre estructuras. Se debe utilizar cuando accedemos continuamente a un mismo valor a traves de un puntero.

Vamos a realizar las ultimas modificaciones sobre el ciclo par intentar optimizarlo un poco mas, si el compilador nos ayuda claro esta.

while (c=*(cptr++))
{
   if (*cptr != 'b')
   { 
      printf("%c",*cptr);
   }
}

while (*cptr) {
c=*cptr;
cptr++;
  mov ecx,dword ptr [ebp-10h] 
  movsx edx,byte ptr [ecx]
  test edx,edx
  je 4 
  mov eax,dword ptr [ebp-10h] 
  mov cl,byte ptr [eax]
  mov byte ptr [ebp-14h],cl
  mov edx,dword ptr [ebp-10h] 
  add edx,1
  mov dword ptr [ebp-10h],edx 
while (c=*(cptr++)) {


  mov edx,dword ptr [ebp-10h] ecx = cptr 
  mov al,byte ptr [edx]
  mov byte ptr [ebp-14h],al   c = *cptr 
  movsx ecx,byte ptr [ebp-14h]
  mov edx,dword ptr [ebp-10h] cptr++ 
  add edx,1
  mov dword ptr [ebp-10h],edx
  test ecx,ecx                c == 0 
  je  4

Como imaginaba, el compilador no ha sido de gran ayuda, ya que ha cargado el valor de cptr en edx por segunda vez sin necesidad; además en vez de utilizar un test al,al que ya contiene el valor de c, ha utilizado ecx resultando en una instruccion más. A pesar de todo el código es una instruccion menor. Eso si aqui se ha hecho una comparación desigual, pero no va mejorar mucho si utilizamos c = *(cptr++);en el código de la izquierda.

No me cabe duda el compilador no optimiza lo mas obvio, es cierto que he compilado sin optimizaciones. De todas formas voy a realizar pruebas utilizando gcc o g++ y expondre mis resultados

No hay comentarios:

Publicar un comentario