Midiendo tiempos y optimizando Campo de estrellas 2D 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: 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): 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 |