FPS en ensamblador

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

Deja un comentario