Exprimiendo el BASIC (I): optimizando el rendimiento en Amstrad CPC

Optimizando rendimiento
Midiendo Rendimiento

Al hacer juegos en BASIC en Amstrad CPC muchas veces podemos acabar abandonando proyectos por falta de velocidad o por falta de espacio. Nuestros ordenadores de 8 bits tienen limitaciones, sí, pero, ¿Es posible mejorar el rendimiento de nuestros juegos? ¿Podemos conseguir que ocupen menos espacio para que quepan más cosas?

En los próximos 2 artículos, os enseñamos unas sencillas ideas para medir rendimiento y memoria en vuestros programas. Con esto podréis probar, reinventar y crear las mejores soluciones para vuestros juegos y proyectos. En este primer artículo hablamos del rendimiento temporal.

Medir el tiempo

Supongamos que tenemos una idea para hacer un juego muy sencillo de laberintos y enemigos. Estamos empezando a hacer pruebas para dibujar el mapa y tecleamos esto:


10 PRINT "####################"
20 PRINT "# # E #VVVV#"
30 PRINT "# # # #* * #"
40 PRINT "@ # # # E | #"
50 PRINT "# # # # * #"
60 PRINT "# # E # >"
70 PRINT "####################"

Al terminar de teclear, se nos ocurre una idea. En las líneas 10 y 70 estamos poniendo 20 veces el carácter almohadilla, pero BASIC tiene la función STRING$, que nos permite repetir varias veces un carácter. Rápidamente, reescribimos estas líneas así:


10 PRINT STRING$(20,"#")
70 PRINT STRING$(20,"#")

El programa hace lo mismo, pero ahora el código parece más cool. Pero, como somos muy curiosos, las dudas nos corroen: ¿Son las dos formas totalmente iguales? ¿Será más rápida una que otra? Ejecutando no notamos ninguna diferencia, ¿Cómo podemos comparar las 2 opciones?

En BASIC tenemos una forma de medir el tiempo, gracias a la instrucción TIME. TIME nos da el tiempo que ha transcurrido desde que se encendió el ordenador, en 300avos de segundo (sí, 300avos, una medida muy rara, pero así funciona). Podéis probarla reseteando la máquina y escribiendo esto:


PRINT TIME

Así, si TIME vale 300 habrá pasado 1 segundo, si vale 600 serán 2 segundos, etc.

Probar y comparar el rendimiento

TIME es una instrucción muy sencilla, pero suficiente para que podamos medir tiempos y saber cuánto tarda una parte de nuestro programa. Apliquémoslo a nuestro programa:


10 t!=TIME
20 PRINT "####################"
30 t!=(TIME-t!)/300
40 PRINT "Tarda: "t!"segundos"

Este programa mide cuánto tarda en ejecutarse el PRINT de 20 almohadillas aproximadamente. Vamos a ver cómo funciona:

  • Línea 10: guardamos en la variable t! lo que vale TIME en ese instante. Esto es como fijarse en la hora de nuestro reloj cuando alguien va a hacer los 100 metros lisos: decimos “YA!” y nos apuntamos mentalmente que nuestro segundero estaba en 15 (por ejemplo).
  • Linea 20: hacemos la tarea que queremos medir (imprimir 20 almohadillas).
  • Linea 30: al terminar la tarea, queremos saber cuánto tiempo ha pasado. Miramos otra vez nuestro reloj (TIME) y restamos los segundos que había antes (t!). Siguiendo el ejemplo, si el segundero ahora marca 26, sabemos que han pasado 11 segundos (26 – 15, es decir, TIMEt!). Pero, ¡Ojo!, el resultado está en 300avos de segundo, que es lo que da TIME. Para tener segundos, dividimos el resultado entre 300 y lo guardamos en t!.
  • Linea 40: para terminar, imprimimos el resultado para poder verlo.

Ya sabemos cuánto tardan nuestros PRINTs originales. Ahora nos falta hacer lo mismo utilizando la función STRING$ para ver cuál tarda más. Sólo tenemos que cambiar la línea 20 por:


20 PRINT STRING$(20,"#")

Ahora sabemos lo que tardan nuestras 2 opciones: 0.07s la primera y 0.07333s la segunda. La diferencia es muy pequeña, pero cualquier milésima que ganemos siempre es importante en nuestras máquinas de 8 bits.

Cuando los tiempos y diferencias son tan pequeños, lo mejor es “ampilar” la forma de comparar. Modificaremos el programa para que repita el dibujado de las 20 almohadillas 100 veces y mediremos el total. De esta forma nos aseguraremos también que estas pequeñas diferencias no son casuales. Aquí tenéis el código:


10 MODE 1: ' Modo 1 de pantalla 40x25
20 DEFINT A-Z: ' Todas las variables enteras
30 veces=100: ' Numero de veces a repetir
' Repetir tantas veces como se quiera la accion que queremos medir
' Cambiar el GOSUB 200 por la llamada a la accion que se quiera
40 t!=TIME
50 for i=0 to veces
60 LOCATE 1,5:GOSUB 200
70 NEXT i
80 t!=(TIME-t!)/300
' Ya hemos medido, ahora imprimimos los resultados
90 PRINT "VECES: "veces
100 PRINT "TIEMPO: "t!"segundos"
110 PRINT "VPS: "veces/t!: ' VPS = Veces por segundo
120 END : ' Fin del programa
'
' Opcion 1: Imprimir las 20 alomhadillas en 1 cadena
'
200 PRINT "#########################"
210 RETURN
'
' Opcion 2: Imprimir usando STRING$
'
300 PRINT STRING$(20,"#")
310 RETURN

En este programa hemos puesto las 2 formas de realizar la tarea (pintar 20 almohadillas) en 2 subrutinas en las líneas 200 y 300. El programa principal repite 100 veces la tarea, llamando con GOSUB, y medimos el tiempo total de las 100 repeticiones. De esta forma, el tiempo total que se tarda es mucho mayor y podemos apreciar que hay importantes diferencias: la primera forma tarda 7.26s, y la segunda 7.35s. Nos queda claro que, en este caso, usar PRINT con la cadena de 20 almohadillas es más rápido que usar PRINT STRING$.

Detalles importantes

Si has observado el código del último ejemplo, verás que las partes principales del programa no tienen comentarios dentro del código de las líneas. Este es un pequeño, pero importante detalle, que sirve para mejorar el rendimiento en BASIC. Aquí os dejo una lista con algunos de estos detalles que podéis experimentar vosotros mismos midiendo el rendimiento:

  • Comentarios en el código: como BASIC es un lenguaje interpretado, los comentarios que ponemos en el código tienen un pequeño efecto en la ejecución. BASIC interpreta la orden REM o el signo en el código, e invierte algo de tiempo en reconocerlo e interpretarlo. Por eso, para medir rendimiento de forma eficiente, no ponemos comentarios en las líneas que queremos medir.
  • Espacios: al igual que los comentarios, BASIC pierde algo de tiempo leyendo e interpretando los espacios en el código. Si queremos optimizar, quitar espacios es una buena idea.
  • Líneas: BASIC también pierde algo de tiempo cada vez que tiene que pasar de una línea a la siguiente. Poner las instrucciones seguidas en una misma línea (separadas por :) también mejora el rendimiento.
  • Uso de variables enteras: BASIC puede manejar números enteros o números reales. Las operaciones con enteros son muchísimo más rápidas que las operaciones con números reales (hasta 10 veces más rápidas en algunos casos). Por eso, casi siempre veréis que nuestros programas usan la instrucción DEFINT, que indica a BASIC qué variables queremos que sean enteras por defecto. Cuando necesitamos una variable de tipo real, le ponemos el sufijo !, como hacemos con t!.

Estos son consejos muy básicos y simples, pero muy efectivos en muchos casos. Tenedlos presentes cuando optimicéis y medir tiempos de vuestros algoritmos para saber qué cambios o estrategias son mejores. En la sección Contribuciones, tenéis algún apunte de utilidad sobre estos consejos.

Ejemplos interesantes

En Fremos siempre estamos probando distintas estrategias y midiendo sus rendimientos para saber cómo mejorar nuestros desarrollos. De las pruebas que hemos hecho, os hemos recopilado algunas de las más interesantes que os pueden ser de utilidad. Encontraréis todas las pruebas en el disco de ejemplos de este artículo. Estas son las pruebas:

  • MIXTEST.BAS: en este programa probamos dos formas distintas de dibujar caracteres mezclados usando modo transparente. La primera prueba utiliza LOCATE, PEN y PRINT, la segunda usa PRINT y caracteres de control. La prueba deja claro que los caracteres de control son mucho más rápidos para dibujar sprites usando caracteres (casi el doble de rápido).
  • SPRTPOKE.BAS: en este ejemplo se prueban distintas formas de dibujar sprites directamente en memoria de vídeo utilizando la instruccion POKE. Probamos hasta 4 formas distintas de pintar el sprite de un marcianito usando POKE y lo comparamos con el dibujado usando caracteres de control y modo transparente. Las 4 formas son: dibujar leyendo desde instrucciones DATA, desde un array de enteros en memoria, desde el array usando un puntero y poniendo los valores a mano en las instrucciones POKE. Los resultados son bastante claros en favor de la opción que usa caracteres de control.
  • SPRTMOD0.BAS: profundizando en el anterior ejemplo, probamos distintas formas de dibujar un sprite en modo 0, usando POKE con los valores puestos a mano. Se utilizan distintas formas de suma parcial que dan distintos resultados. Además, todo está hecho en modo 0, que es el doble de lento que modo 1 porque hay que escribir el doble de bytes en memoria para un sprite del mismo tamaño (por los 16 colores).

Esperamos que estos ejemplos os gusten y os sean de ayuda en el desarrollo de vuestros propios juegos. Desde Fremos os animamos a que hagáis experimentos y pruebas de rendimiento y nos las enviéis para que podamos compartirlas con todos y aprender un poco más :).

Contribuciones

Toolkit Basic Programmer's Aid
Menú del toolkit

MiguelSky, creador de CPCGamesCD, nos ha recomendado un interesante software de 1985:

Se trata de una ROM que instala unos cuantos comandos RSX para ser usados desde BASIC muy útiles para programar. Los comandos nos permiten buscar y reemplazar, copiar y mover líneas de BASIC, renumerado avanzado o listar programas que están en disco sin alterar el que hay en memoria, entre otros. De todos estos comandos, hay uno que destaca especialmente: |PACK. Este comando coge el programa BASIC en memoria y lo compacta eliminando comentarios y espacios, juntando líneas y acortando los nombres de variable, entre otras cosas.

Sin duda, este software de BeeBugSoft es un conjunto de fantásticas utilidades muy prácticas para editar y optimizar programas en BASIC directamente en nuestros Amstrads. Muchas gracias a MiguelSky por esta estupenda recomendación.

Material

Deja un comentario