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).