Tech News
← Back to articles

Efficient Basic Coding for the ZX Spectrum

read original related products more articles

[Click here to read this in English ]

El intérprete de lenguaje Sinclair BASIC incluido en la ROM del ZX Spectrum es, en muchos aspectos, una maravilla del software, concretamente de la programación en ensamblador, y daría para hablar durante mucho tiempo. En esta serie queremos destacar los puntos más importantes a tener en cuenta para que los programas escritos en ese lenguaje sean lo más eficientes posibles, en primer lugar en tiempo de ejecución, pero también en espacio ocupado en memoria.

En esta primera entrega de la serie trataremos de las líneas de dichos programas; más allá de la necesidad de numerarlas, algo que no se hace desde hace décadas en ningún lenguaje de programación, está el propio hecho de la eficiencia del intérprete a la hora de manejarlas.

Antes de meternos en el meollo, conviene resumir los límites que existen en esta máquina relativos a las líneas de programa:

Las líneas de programa, una vez éste queda almacenado en la memoria listo para su ejecución, ocupan 2 bytes (por cierto, almacenados en formato big-endian, el único caso de este formato en el ZX). Esto podría llevar a pensar que tenemos disponibles desde la línea 0 a la 65535 (el máximo número que puede almacenarse en 2 bytes), pero no es exactamente así. A la hora de editar manualmente un programa sólo se nos permite numerar las líneas desde 1 a 9999 . Si el programa es manipulado fuera del editor (se puede hacer con POKE ), es posible tener la línea 0, y ésta aparecer al listarlo, pero no será editable. De la misma manera (manipulando el programa con POKE ) se pueden numerar líneas por encima de la 9999; sin embargo, esto causará problemas en ejecución: muchas sentencias del lenguaje que admiten un número de línea como parámetro, como GO TO o RESTORE , dan error si la línea es mayor de 32767 ; la pila de llamadas dejará de funcionar correctamente si se hace un GO SUB a una línea mayor de 15871 (3DFF en hexadecimal); el intérprete reserva el número de línea 65534 para indicar que está ejecutando código escrito en el buffer de edición (y no en el listado del programa); por último, listar programas por pantalla tampoco funciona bien con líneas mayores de 9999, y en cuanto las editemos manualmente volverán a quedar con sólo 4 dígitos decimales.

(por cierto, almacenados en formato big-endian, el único caso de este formato en el ZX). Esto podría llevar a pensar que tenemos disponibles desde la línea 0 a la 65535 (el máximo número que puede almacenarse en 2 bytes), pero no es exactamente así. un programa sólo se nos permite numerar las líneas desde a . Si el programa es manipulado fuera del editor (se puede hacer con ), es posible tener la línea 0, y ésta aparecer al listarlo, pero no será editable. De la misma manera (manipulando el programa con ) se pueden numerar líneas por encima de la 9999; sin embargo, esto causará problemas en ejecución: muchas sentencias del lenguaje que admiten un número de línea como parámetro, como o , ; la dejará de funcionar correctamente si se hace un a una línea mayor de 15871 (3DFF en hexadecimal); el intérprete para indicar que está ejecutando código escrito en el buffer de edición (y no en el listado del programa); por último, listar programas por pantalla tampoco funciona bien con líneas mayores de 9999, y en cuanto las editemos manualmente volverán a quedar con sólo 4 dígitos decimales. La longitud en bytes de cada línea de programa se almacena justo después del número de línea, ocupando 2 bytes (esta vez en little-endian). Esta longitud no incluye ni el número de línea ni la longitud en sí misma. Por tanto, podríamos esperar poder tener líneas de un máximo de 65535 bytes en su contenido principal (menos 1, porque siempre tiene que haber un 0x0D al final para indicar el fin de línea); asimismo, las líneas más cortas ocuparán en memoria 2+2+1+1 = 6 bytes: serían aquéllas que contienen una sola sentencia que no tiene parámetros, p.ej., 10 CLEAR . Una rutina muy importante en la ROM del Spectrum, la encargada de buscar la siguiente línea o la siguiente variable saltándose la actual (llamada NEXT-ONE y situada en la dirección 0x19B8) funciona perfectamente con rangos de tamaño de línea entre 0 y 65535, pero en ejecución el intérprete dejará de interpretar una línea en cuanto se encuentre un 0x0D al comienzo de una sentencia (si la línea es más larga, por ejemplo porque se haya extendido mediante manipulaciones externas, ignorará el resto, por lo que puede ser usado ese espacio para almacenar datos dentro del programa). Más importante aún: dará error al tratar de ejecutar más de 127 sentencias en una misma línea, es decir, una línea en ejecución sólo puede tener, en la práctica, desde 1 hasta 127 sentencias.

Una vez resumidos los datos básicos sobre las líneas y los números de línea, nos centraremos en una característica muy concreta del intérprete de BASIC que resulta fundamental para conseguir incrementar su eficiencia en la ejecución de programas:

El intérprete no usa una tabla indexada de líneas de programa

Los programas BASIC del ZX se pre-procesan nada más teclearlos (tras teclear líneas completas en el caso del ZX Spectrum +2 y superiores), lo que ahorra espacio en ROM al evitar el analizador léxico que haría falta posteriormente. En ese pre-proceso no sólo se resumen palabras clave de varias letras en un sólo byte, es decir, se tokeniza (qué palabro más feo), sino que se aprovecha para insertar en los lugares más convenientes para la ejecución algunos elementos pre-calculados: un ejemplo es el propio tamaño en memoria de cada línea, como se ha explicado antes, pero también se almacenan silenciosamente los valores numéricos de los literales escritos en el texto (justo tras dichos literales), y se reservan huecos para recoger los argumentos de las funciones de usuario (justo tras los nombres de los correspondientes parámetros en la sentencia DEF FN ), por ejemplo.

Lo que nunca, nunca se hace es reservar memoria para almacenar una tabla con las direcciones en memoria de cada línea de programa. Es decir, una tabla que permita saber, a partir de un número de línea y con complejidad computacional constante (tardando siempre lo mismo independientemente del número de línea, lo que formalmente se escribe O(1)), el lugar de memoria donde comienza el contenido tokenizado de dicha línea, para poder acceder rápidamente a las sentencias correspondientes y ejecutarlas.

... continue reading