En FPS con GRAPH.LIB, habíamos concluido con el análisis y resultados de diferentes implementaciones gráficas en C bajo DOS.
Os adelantaba que en ensamblador la diferencia de rendimiento no iba a ser demasiado elevada, y que el motivo era que la implementación de memcpy, era ya muy eficiente en los compiladores de C.
Pero me apetecía probar Jwasm, por lo que a pesar de todo lo implementé en ensamblador. Me lo estaba pasando bien, así que le apliqué algunos trucos que espero que os resulten interesantes.
Lo primero es que estoy mezclando instrucciones de 8, 16 y 32 bits. En general lo más veloz es usar de 32 bits, pero éstas van a consumir más espacio y memoria, por lo que salvo que estuviera en un punto crítico, he usado el menor tamaño posible para hacer la operación.
He utilizado a propósito las macros de estructuras de control .repeat/.until. cuando fuera posible sin penalizar la eficiencia, y me hubiera gustado dar cabida a .if/.elseif/.else/.endif, .while/.endw, .repeat/.untilcxz, .break, etc, pero no ha sido posible.
Si he introducido otras macros que facilitan la lectura, y sobre todo el comenzar un programa desde cero: .code, .startup, .exit, rept, …
Gracias a lo compacto del código, que finalmente queda en 194 bytes, podemos conseguir algo que a priori puede parecer imposible. Hacerlo caber en un archivo COM, que por diseño están limitados a un total de 64 Kb. incluyendo código, pila y datos. Recordemos que solamente de datos gráficos se están manejando 64.768 bytes entre la imagen y su paleta. En este aspecto, si que ensamblador gana de calle, veámoslo:
- Ensamblador 16 bits: 64.962 bytes (código 194 bytes).
- Portabilidad C 16 bits: 71.632 bytes (código 6.864 bytes).
- BIOS C 16 bits: 71.672 bytes (código 6.904 bytes).
- Original C 16 bits: 78.122 bytes (código 13.354 bytes).
- Graph.lib 16 bits: 90.906 bytes (código 26.138 bytes).
Es decir, más de 100 lineas de listado, se acaban transformando en algo menos de 200 bytes de código máquina ejecutable. ¡Todo un logro!
Por supuesto ese resultado incluye interacciones con DOS, BIOS, hardware, e incluso una implementación simplificada de itoa para la conversión de números. Pese a que el programa se ensambla para DOS, la primera versión la desarrollé sobre Linux (Kubuntu 13.04), y la segunda sobre Windows (8 x64). Ventajas del ensamblado cruzado.
En cuanto a velocidad, ha anotado una marca de 185 FPS, no llega a ser un 1% más rápida que la versión de 32 bits posteriormente ajustada, dado que como anticipaba, el único beneficio viene de unas operaciones más eficientes a la hora de comprobar el teclado, y de usar registros que permite ensamblador, aunque la velocidad de volcado de frames, sea equivalente.
Un detalle curioso, que viene a colación, es que el programa funciona lógicamente en modo real, aunque use instrucciones i386 de 32 bits, es decir, eventualmente debería ser todavía más compatible que la versión en modo protegido, que requería un extensor DOS.
.model tiny .stack 64 .386 .data gacPerin db ... iStart dw 0 iStop dw 0 .code org 100h .startup ;Segmento ES apuntando a framebuffer mov ax, 0A000h mov es, ax ;Modo 13 mov ax, 13h int 10h ;Asignar toda la paleta xor cx, cx lea si, gacPerin[64000] .repeat mov dx, 3c8h mov al, cl out dx, al ;El DAC espera los valores en el rango 0..63, por lo que debemos dividir cada uno por 4 (0..255). mov dx, 3c9h rept 3 lodsb shr al, 2 out dx, al endm inc cx .until cx >= 256 ;cmp cx, 256; jb setpal ;Contador de inicio xor ah, ah int 1ah mov iStart, dx xor bp, bp ;frames en bp .repeat ;Volcar al framebuffer 320x200 pixeles lea si, gacPerin xor di, di mov cx, 64000/4 rep movsd inc bp ;frames++ ;Comprobar pulsación de tecla mov ah, 1 int 16h .until !zero? ;jz ;Leer tecla del buffer xor ah, ah int 16h ;Contador de fin xor ah, ah int 1ah mov iStop, dx ;Modo texto mov ax, 3h int 10h ;Escribir frames mov ax, bp lea di, gacPerin call itoa mov ah, 09h lea dx, gacPerin int 21h ;Escribir FPS mov ax, bp mov bx, 18 mul bx ;ax=frames*18 xor dx, dx mov bx, iStop sub bx, iStart div bx ;dx:ax=ax/(iStop-iStart lea di, gacPerin call itoa mov ah, 09h lea dx, gacPerin int 21h ;Salir a DOS .exit ;mov ah, 4ch; int 21h ;void itoa(char *pacbuffer = DI, int number = AX) itoa proc ;Vaciar buffer destino mov cx, 10 itoa_set0: mov [edi+ecx-1], byte ptr 0 loop itoa_set0 mov [di+10], byte ptr 10 mov [di+11], byte ptr '$' mov cx, 9 ;contador mov bx, 10 ;base .repeat xor dx, dx div bx ;edx=eax%10; eax=eax/10 add dx, '0' ;carácer mov [edi+ecx], dl ;dígito dec cx ; test ax, ax .until zero? ;jnz ret itoa endp end |
Podéis descargar el código fuente y el ejecutable aquí (57 Kb. en formato ZIP).