Como excusa para revisar los progresos de los snapshots diarios (27 MB en formato TAR.BZ2 vía FTP), decidí unificar los fuentes de 100 FPS en un PC (16 bits) y 500 FPS en un PC, para mostrar el funcionamiento del preprocesador de C, de cara a mantener la portabilidad entre plataformas.
El artículo pretendía explicar el uso de #define, #ifdef/#if defined, pero incitado de nuevo por los poderes ocultos de Watcom, me dediqué a ver hasta cuánto podía mejorar el original, sin necesidad de usar ensamblador. ¡Qué tiempos con el #pragma aux!
Lo primero que hice fue sustituir las llamadas a kbhit y getch, por una implementación directa basada en los servicios de teclado de la BIOS. Este cambio redujo en algunos bytes el tamaño final del ejecutable, al evitar enlazar con esas dos funciones de la RTL, y al reducir la sobrecarga del bucle, también aumentó levemente su rendimiento.
Después decidí que la versión de 16 bits usara el modelo de memoria small en vez de compact, esto aumentaría la velocidad de ejecución general, y reduciría también otro tanto el tamaño del programa. Lo malo es que para acceder a la memoria de vídeo, que está en otro segmento, se necesita un puntero far, y el uso de _fmemcpy.
Apliqué la optimización propagación de constante en el fragmento que asigna la paleta. Sorprendentemente era una optimización que Watcom no había aplicado demasiado bien, y de nuevo reducimos espacio, y ganamos algo de rendimiento en el proceso.
También sustituí printf por cprintf, que es más veloz y compacto, debido a que no soporta redirecciones DOS.
Para la versión de 32 bits, usé PMODE/WI 1.33, aunque no es tan potente como DOS32/A o DOS/4GW, era suficiente, y ahorró casi 20 Kb.
Finalmente ajusté un poco las opciones de compilación, y generé el código con Open Watcom 2.0 Beta, el resultado fueron ejecutables levemente más grandes.
Los resultados finales fueron:
16 bits:
– Versión original: 92 FPS / 78.122 bytes.
– Versión ajustada: 93 FPS / 71.632 bytes.
32 bits:
– Versión original: 180 FPS / 111.934 bytes.
– Versión original: 184 FPS / 87.188 bytes.
Es decir, una mejora de rendimiento de entorno al 2%, y de ahorro de espacio de entre el 9% y el 28%. Teniendo en cuenta que los ajustes llevaron casi el mismo tiempo que el desarrollo inicial, eso nos da una idea del coste de este tipo de mejoras.
Cabe destacar que si consideramos que casi 64 Kb. corresponden a datos, el código resultante de la versión optimizada de 16 bits quede reducido a apenas unos 6 Kb.
Lógicamente en cuanto a tiempo de ejecución, lo más eficaz habría sido reemplazar memcpy, en realidad el cuello de botella del programa. Paul Hsieh, tiene versiones muy interesantes, pero que parten de la base de o bien optimizar para una arquitectura concreta, o bien utilizar instrucciones extendidas tipo x87, MMX o SSE.
Ahora vamos a la parte de portabilidad. Si echáis un vistazo al código de abajo, veréis algunas novedades sobre las versiones anteriores.
Dentro del bloque de #if (defined(__FLAT__)), es decir modo flat, usado sobre DOS en modo protegido, veréis que:
– Se mapea int86 a int386: Una incómoda particularidad de algunos compiladores de 32 bits, donde la invocación de interrupciones usa dos versiones distintas, dependiendo del ancho de los registros.
– Se define far a nada: En modo flat, no hay direccionamiento far, y todo es near con punteros de 32 bits. El modificador far, es necesario para indicarle al compilador que los datos de la imagen en 16 bits, son lejanos y están en otro segmento. Es una consecuencia de usar la optimización del modo de memoria small, donde todo es near por defecto, y cuando no lo es, debe indicarse explícitamente.
– Se implementa la macro MK_FAR: A semejanza de MK_FP en modo real, crea un puntero de 32 bits en base a un segmento y un offset de 16 bits en modo real.
En modo real, es decir, cuando no está definido flat, lo que hacemos es:
– Mapeamos memcpy a _fmemcpy: _fmemcpy opera siempre con punteros far, así que es requerida cuando origen o destino sean lejanos. Por defecto el compilador utilzará las funciones adecuadas al modelo de memoria que estemos usando, de nuevo aquí, estamos mezclando el modelo de memoria small con punteros near, con punteros far, así que debemos declararlo explícitamente.
– Definimos MK_FAR: Een este caso, como un simple alias de MK_FP.
#include
#include
#include
#include
#include
#ifdef __WATCOMC__
#include
#else
#include
#endif
#if (defined(__FLAT__))
#define int86 int386
#define far
#define MK_FAR(seg, off) (((seg)<<4)+(off))
#else
#define memcpy _fmemcpy
#define MK_FAR(seg, off) (MK_FP((seg), (off)))
#endif
static unsigned char far gacPerin[]={...};
int main (void)
{
register unsigned int iX, iX3;
union REGS udtRegs;
clock_t clkStart, clkEnd;
/* Modo 13 */
udtRegs.w.ax=0x13;
int86(0x10, &udtRegs, &udtRegs);
iX=0;
for (iX3=sizeof(gacPerin)-768; iX3>2);
outp(0x3C9, gacPerin[iX3++]>>2);
outp(0x3C9, gacPerin[iX3++]>>2);
}
iX=0;
clkStart=clock();
do
{
/* Volcar al framebuffer 64000 bytes */
memcpy((unsigned char far *) MK_FAR(0xA000, 0), gacPerin, sizeof(gacPerin)-768);
iX++;
/* Comprobar pulsación de tecla */
udtRegs.w.ax=0x0100;
int86(0x16, &udtRegs, &udtRegs);
}
while (udtRegs.h.al==0);
clkEnd=clock();
/* Leer tecla del buffer */
udtRegs.h.ah=0x00;
int86(0x16, &udtRegs, &udtRegs);
/* Modo texto */
udtRegs.w.ax=0x3;
int86(0x10, &udtRegs, &udtRegs);
/* Mostrar en pantalla número de cuadros, y FPS */
cprintf("Frames: %d; FPS: %d\n", iX, ((long) iX*CLOCKS_PER_SEC)/(clkEnd-clkStart));
return(0);
}
El objetivo era que se entendiera la forma de trabajar con diferentes plataformas. Hemos visto que es sencillo, pero que se requiere un conocimiento previo sobre como trabaja cada una de ellas, de manera que podamos aislar las particularidad.
El resultado podemos considerarlo magnífico, un código más portable, más veloz, y más óptimo.
Puedes descargar el código fuente y los ejecutables aquí (101 Kb. en formato ZIP).