Midiendo tiempos y optimizando Campo de estrellas 2D

Click here to see in English

En el tutorial anterior Campo de estrellas 2D aprendimos como hacer un sencillo y bonito efecto de campo de estrellas. Y vimos que está limitado a 40 estrellas simultaneas, ya que si ponemos más empiezan los tirones y habría que optimizar el código... En este tutorial vamos a aprender a medir facilmente tiempos y optimizaremos el programa todo lo que podamos.

Para medir el tiempo, normalmente se toma una rutina, se analiza el ensamblador y se cuentan los ciclos por instrución , etc... un royo vamos. Hay maneras más rápidas de medir un bloque de codigo o de contar los frames/loops por segundo que alcanza un programa. Para ello nos vamos a apoyar en un comando del firmware del Amstrad CPC llamado KL TIME PLEASE (BD0D) que devuelve un contador con el tiempo transcurrido desde que el equipo se encendió o reseteó (en unidades de 1/300 segundos, es decir 3.3333ms). Este contador es de 32bits (4 bytes), y se da la vuelta (empieza de 0 otra vez) transcurridos aproximadamente 166 días (32bits = 4294967296 / 300 = 14316557s / 86400sdía = 165,7 días). Como nosotros vamos a hacer mediciones mucho más pequeñas, nos vale con usar únicamente 2 bytes, que nos permiten medir tiempos de hasta 3.6 minutos. Este método de medición es valido siempre y cuando no deshabilitemos las interrupciones en ningún momento, ya que deja de incrementarse el contador. En el momento de escribir este tutorial, este método no puede usarse con z88dk, ya que siempre están deshabilitadas las interrupciones (ya les he notificado el problema), por lo que lo vamos a usar desde sdcc.

Para leer el valor del contador, nos hacemos una sencilla función:

////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////

Para medir los frames/loops por segundo que da el programa, simplemente vamos ir sumando las vueltas que da al bucle borrar/mover/pintar y cada segundo mostrar el valor por pantalla, el codigo fuente completo quedaría así (se puede descargar al final):

////////////////////////////////////////////////////////////////////////
// star01.c
// Optimizing a 2D Star Field
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void SetMode0PixelColor(unsigned char *pByteAddress, unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = *pByteAddress;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  *pByteAddress = nByte;
}

void PutPixelMode0(unsigned char nX, unsigned char nY, unsigned char nColor)
{
  unsigned char nPixel = 0;
  unsigned int nAddress = 0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 2);
  nPixel = nX % 2;

  SetMode0PixelColor((unsigned char *)nAddress, nColor, nPixel);
}

////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////

struct _tStar
{
  unsigned char nX;
  unsigned char nY;
  unsigned char nStarType;
};

#define STARS_NUM 40
struct _tStar aStars[STARS_NUM];

void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nStar = 0;
  memset(aStars, 0, sizeof(aStars));
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //PALETE
  __asm
    ld a, #0
    ld b, #0 ;black
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #1
    ld b, #12 ;Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #2
    ld b, #25 ;Pastel Yellow    
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #3
    ld b, #24 ;Bright Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;

  //Init
  for(nStar = 0; nStar < STARS_NUM; nStar++)
  {
    aStars[nStar].nX = rand() % 160;
    aStars[nStar].nY = rand() % 200;
    aStars[nStar].nStarType = rand() % 3;
  }

  nTimeLast = GetTime();

  while(1)
  {
    for(nStar = 0; nStar < STARS_NUM; nStar++)
    {
      //delete star
      PutPixelMode0(aStars[nStar].nX, aStars[nStar].nY, 0);

      //move star
      switch(aStars[nStar].nStarType)
      {
        case 0: //slow star
          aStars[nStar].nX += 1;
          break;
        case 1: //medium star
          aStars[nStar].nX += 2;
          break;
        case 2: //fast star
          aStars[nStar].nX += 3;
          break;
      }
      
      if(aStars[nStar].nX >= 160)
      {
        aStars[nStar].nX = 0;
        aStars[nStar].nY = rand() % 200;
        aStars[nStar].nStarType = rand() % 3;
        continue;
      }

      //paint star
      PutPixelMode0(aStars[nStar].nX, aStars[nStar].nY, aStars[nStar].nStarType + 1);
    }

    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {

      //TXT SET CURSOR 0,0
      __asm
        ld h, #1
        ld l, #1
        call #0xBB75
      __endasm;

      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si lo compilamos y ejecutamos en un emulador nos muestra lo siguiente:

emulator

Es decir, el programa está funcionando a 29 frames por segundo. Si aumentamos las estrellas a 80, vemos que baja a 15 frames por segundo y si aumentamos a 200 estrellas vemos que baja a 6 frames por segundo... Para ver que parte está consumiendo mucho tiempo, podemos por ejemplo probar a quitar el pintado, comentando las dos llamadas a PutPixelMode0 (la de borrar y la de pintar), quedando únicamente el codigo que actualiza la posición de las estrellas, veremos que la velocidad sube hasta unos 110 frames por segundo. Si por el contrario dejamos el borrado/pintado de los pixeles pero comentamos el codigo que actualiza la posición de las estrellas veremos que la velocidad sólo alcanza 35 frames por segundo. Queda claro que el principal problema está en el borrado/pintado.

¿Que podemos hacer para optimizar este código? Pues a simple vista se ven varias cosas que optimizar, por ejemplo, cuando vamos a borrar una estrella, sabemos que el color es 0, pero seguimos llamando a PutPixelMode0 que a su vez llama a SetMode0PixelColor que para poner a 0 hace varias operaciones a nivel de bit, que podriamos evitar simplemente poniendo todo el byte a 0 sin más. Por otro lado, cuando vamos a borrar una estrella, al llamar a PutPixelMode0 se vuelve a calcular la posición en la memoria de video (con varias sumas, multiplicaciones y divisiones) cuando realmente ya la habiamos calculado antes, cuando se pintó la estrella. Podriamos simplemente almacenarla al pintar y así no habría que volver a calcularla para borrar.

Si implementamos en el código estas optimizaciones nos quedaría así:

////////////////////////////////////////////////////////////////////////
// star02.c
// Optimizing a 2D Star Field
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void SetMode0PixelColor(unsigned char *pByteAddress, unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = *pByteAddress;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  *pByteAddress = nByte;
}

unsigned char *PutPixelMode0(unsigned char nX, unsigned char nY, unsigned char nColor)
{
  unsigned char nPixel = 0;
  unsigned int nAddress = 0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 2);
  nPixel = nX % 2;

  SetMode0PixelColor((unsigned char *)nAddress, nColor, nPixel);
  return (unsigned char *)nAddress;
}

////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////

struct _tStar
{
  unsigned char nX;
  unsigned char nY;
  unsigned char nStarType;
  unsigned char *pVideoAddress;
};

#define STARS_NUM 40
struct _tStar aStars[STARS_NUM];

void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nStar = 0;
  memset(aStars, 0, sizeof(aStars));
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //PALETE
  __asm
    ld a, #0
    ld b, #0 ;black
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #1
    ld b, #12 ;Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #2
    ld b, #25 ;Pastel Yellow    
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #3
    ld b, #24 ;Bright Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;

  //Init
  for(nStar = 0; nStar < STARS_NUM; nStar++)
  {
    aStars[nStar].nX = rand() % 160;
    aStars[nStar].nY = rand() % 200;
    aStars[nStar].nStarType = rand() % 3;
    aStars[nStar].pVideoAddress = (unsigned char *)0xC000;
  }

  nTimeLast = GetTime();

  while(1)
  {
    for(nStar = 0; nStar < STARS_NUM; nStar++)
    {
      //delete star
      *aStars[nStar].pVideoAddress = 0;

      //move star
      switch(aStars[nStar].nStarType)
      {
        case 0: //slow star
          aStars[nStar].nX += 1;
          break;
        case 1: //medium star
          aStars[nStar].nX += 2;
          break;
        case 2: //fast star
          aStars[nStar].nX += 3;
          break;
      }
      
      if(aStars[nStar].nX >= 160)
      {
        aStars[nStar].nX = 0;
        aStars[nStar].nY = rand() % 200;
        aStars[nStar].nStarType = rand() % 3;
        continue;
      }

      //paint star
      aStars[nStar].pVideoAddress = PutPixelMode0(aStars[nStar].nX, aStars[nStar].nY, aStars[nStar].nStarType + 1);
    }

    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      //TXT SET CURSOR 0,0
      __asm
        ld h, #1
        ld l, #1
        call #0xBB75
      __endasm;

      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Si lo ejecutamos en el emulador veremos que los frames por segundo aumentan a 41, más de un 33% de mejora respecto al código original (que daba 29)...

¿Se puede optimizar más? Mucho más, desde luego. Por ejemplo, cuando una estrella llega a la derecha del todo, se la mueve de nuevo a la posición x=0 y se hacen un par de rand() para asignarle aleatoriamente otra velocidad y posición en y, pues bien, estas llamadas a rand() son costosas, así que si las quitamos vemos que suben los frames por segundo a 45, es decir estas dos asignaciones aleatorias nos costaban 4 frames por segundo...

¿Se puede optimizar más? Por supuesto y mucho. Quedan un par de cosas importantes que optimizar, la primera de ellas es que cada vez que vamos pintar un pixel y llamamos a PutPixelMode0 se calcula su dirección completa en la memoria de video, pero resulta que una estrella únicamente se mueve en horizontal, por lo que su valor en el eje y es siempre constante y estamos haciendo varias multiplicaciones, divisiones y sumas que son constantes... La segunda optimización que se puede hacer es cambiar todos los accesos en el bucle principal a aStars[nStar], por un puntero a la estructura en concreto.

Si implementamos en el código estas optimizaciones nos quedaría así:

////////////////////////////////////////////////////////////////////////
// star03.c
// Optimizing a 2D Star Field
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char GetMode0PixelColorByte(unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = 0;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  return nByte;
}

unsigned char *GetLineAddress(unsigned char nLine)
{
  return (unsigned char *)0xC000 + ((nLine / 8) * 80) + ((nLine % 8) * 2048);
}

////////////////////////////////////////////////////////////////////////
unsigned char char1,char2,char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////

struct _tStar
{
  unsigned char nX;
  unsigned char nY;
  unsigned char nStarType;
  unsigned char *pLineAddress;
  unsigned char *pCurrentAddress;
};

#define STARS_NUM 40
struct _tStar aStars[STARS_NUM];

void main()
{
  unsigned int nFPS = 0;
  unsigned int nTimeLast = 0;
  unsigned char nStar = 0;
  struct _tStar *pStar = NULL;
  memset(aStars, 0, sizeof(aStars));
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //PALETE
  __asm
    ld a, #0
    ld b, #0 ;black
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #1
    ld b, #12 ;Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #2
    ld b, #25 ;Pastel Yellow    
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #3
    ld b, #24 ;Bright Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;

  //Init
  for(nStar = 0; nStar < STARS_NUM; nStar++)
  {
    aStars[nStar].nX = rand() % 160;
    aStars[nStar].nY = rand() % 200;
    aStars[nStar].nStarType = rand() % 3;
    aStars[nStar].pLineAddress = GetLineAddress(aStars[nStar].nY);
    aStars[nStar].pCurrentAddress = aStars[nStar].pLineAddress;
  }

  nTimeLast = GetTime();

  while(1)
  {
    for(nStar = 0; nStar < STARS_NUM; nStar++)
    {
      pStar = &aStars[nStar];
      //delete star
      *pStar->pCurrentAddress = 0;

      //move star
      switch(pStar->nStarType)
      {
        case 0: //slow star
          pStar->nX += 1;
          break;
        case 1: //medium star
          pStar->nX += 2;
          break;
        case 2: //fast star
          pStar->nX += 3;
          break;
      }
      
      if(pStar->nX >= 160)
      {
        pStar->nX = 0;
        //pStar->nY = rand() % 200;
        //pStar->nStarType = rand() % 3;
        continue;
      }

      //paint star
      pStar->pCurrentAddress = pStar->pLineAddress + (pStar->nX / 2);
      *pStar->pCurrentAddress = GetMode0PixelColorByte(pStar->nStarType + 1, pStar->nX % 2);
    }

    nFPS++;

    if(GetTime() - nTimeLast >= 300)
    {
      //TXT SET CURSOR 0,0
      __asm
        ld h, #1
        ld l, #1
        call #0xBB75
      __endasm;

      printf("%u  ", nFPS);

      nTimeLast = GetTime();
      nFPS = 0;
    }
  }
}
////////////////////////////////////////////////////////////////////////

Con estos cambios alcanzamos la friolera de 77 frames por segundo (por encima de la capacidad de pintado del CPC), así que hemos conseguido duplicar (y más) los frames por segundo de cuando empezamos. Para finalizar, limpiamos el código y los carteles para medir tiempos y duplicamos el número de estrellas de 40 a 80 y obtenemos un efecto de campo de estrellas con el doble de estrellas y más velocidad que cuando empezamos:

////////////////////////////////////////////////////////////////////////
// star04.c
// Optimizing a 2D Star Field
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char GetMode0PixelColorByte(unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = 0;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  return nByte;
}

unsigned char *GetLineAddress(unsigned char nLine)
{
  return (unsigned char *)0xC000 + ((nLine / 8) * 80) + ((nLine % 8) * 2048);
}

struct _tStar
{
  unsigned char nX;
  unsigned char nY;
  unsigned char nStarType;
  unsigned char *pLineAddress;
  unsigned char *pCurrentAddress;
};

#define STARS_NUM 80
struct _tStar aStars[STARS_NUM];

void main()
{
  unsigned char nStar = 0;
  struct _tStar *pStar = NULL;
  memset(aStars, 0, sizeof(aStars));
  
  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  //PALETE
  __asm
    ld a, #0
    ld b, #0 ;black
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #1
    ld b, #12 ;Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #2
    ld b, #25 ;Pastel Yellow    
    ld c, b
    call #0xBC32 ;SCR SET INK

    ld a, #3
    ld b, #24 ;Bright Yellow
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;

  //Init
  for(nStar = 0; nStar < STARS_NUM; nStar++)
  {
    aStars[nStar].nX = rand() % 160;
    aStars[nStar].nY = rand() % 200;
    aStars[nStar].nStarType = rand() % 3;
    aStars[nStar].pLineAddress = GetLineAddress(aStars[nStar].nY);
    aStars[nStar].pCurrentAddress = aStars[nStar].pLineAddress;
  }

  while(1)
  {
    for(nStar = 0; nStar < STARS_NUM; nStar++)
    {
      pStar = &aStars[nStar];
      //delete star
      *pStar->pCurrentAddress = 0;

      //move star
      switch(pStar->nStarType)
      {
        case 0: //slow star
          pStar->nX += 1;
          break;
        case 1: //medium star
          pStar->nX += 2;
          break;
        case 2: //fast star
          pStar->nX += 3;
          break;
      }
      
      if(pStar->nX >= 160)
      {
        pStar->nX = 0;
        //pStar->nY = rand() % 200;
        //pStar->nStarType = rand() % 3;
        continue;
      }

      //paint star
      pStar->pCurrentAddress = pStar->pLineAddress + (pStar->nX / 2);
      *pStar->pCurrentAddress = GetMode0PixelColorByte(pStar->nStarType + 1, pStar->nX % 2);
    }
  }
}
////////////////////////////////////////////////////////////////////////

Que si lo ejecutamos en el emulador obtendremos algo parecido a esto (mucho más fluido en el emulador):

stars

Como hemos visto, la optimización se consigue evitando cálculos innecesarios o que podemos tener precalculados. Como alguien dijo "el cálculo que menos tiempo consume es el que no se hace" :-)

Podéis bajar un zip con todos ficheros (código fuente, bat's para compilar, binarios y dsk's) aquí: Midiendo_tiempos_y_optimizando_Campo_de_estrellas_2D.zip

 

 

www.CPCMania.com 2012