Efficient BASIC coding for the ZX Spectrum (IV)

[Click here to continue in English ]

Éste es el cuarto y último de una serie de artículos que explican los fundamentos de la (in)eficiencia de los programas en BASIC puro para el ZX Spectrum:

I. Sobre los números de línea

II. Sobre las variables

III. Sobre las expresiones

IV. Funcionalidades importantes y medida del tiempo

En esta última entrega hablaremos sobre la eficiencia de algunas funcionalidades importantes que ofrece el lenguage BASIC del ZX, así como de la medida del tiempo de ejecución en los programas.

Dibujar

Especialmente sensibles en cuanto a velocidad de ejecución son las sentencias gráficas de dibujo (PLOT, DRAW, CIRCLE), como sabe cualquiera que haya programado lo más mínimo en este lenguaje. DRAW cuando dibuja arcos, y CIRCLE siempre, utilizan cálculos en coma flotante repetidamente, lo que las hace muy lentas a pesar de que sus implementaciones en la ROM están optimizadas. PLOT no es ineficiente en sí mismo (si se le pasan expresiones enteras, evitando así el cálculo en coma flotante), aunque para dibujar con PLOT algo decente hay que hacer muchos PLOT, y, por tanto, el coste se multiplica. DRAW utiliza el algoritmo de Bresenham para líneas rectas, el cual es bastante eficiente porque además no llama al calculador, pero, al igual que el resto de rutinas, tiene que establecer también los atributos de pantalla por los que pasa, además de el modo (INVERSE, OVER), lo que lo enlentece. En resumen:

Las rutinas de dibujo —y las de consulta gráfica, como POINT— deberían usarse esporádicamente, con argumentos enteros, y en lugares que no tengan requisitos de velocidad elevados, dando preferencia al dibujo rectilíneo.

Escribir

A la hora de imprimir caracteres en pantalla es también muy importante acelerar, porque ésta es una tarea esencial en cualquier programa. Si tenemos que imprimir, por ejemplo, un sprite que tenga varios caracteres de alto y de ancho (típicamente definidos por el usuario, o UDGs), no parece conveniente hacer dos bucles FOR anidados para ello, con todo el cómputo extra y espacio en memoria de programa que eso supone. Sería mucho mejor usar dentro del sprite caracteres de control que muevan el cursor de impresión conforme el sprite se imprime.

Ay, lamentablemente el intérprete del ZX no implementa bien los caracteres de control de movimiento direccional (las flechas). Por tanto, para solventar el problema de la lentitud de los bucles que dibujan el sprite sólo nos quedan técnicas como el desenrollado de bucles, explicada en la primera entrega de esta serie.

Aquí dejo un ejemplo de programa que, al ejecutar, ilustra algunos problemas que tiene el intérprete de la ROM con los caracteres de control direccionales:

10  BORDER 1 : CLS

20  PRINT AT 10 , 10 ; PAPER 5 ; “0” ; PAPER 7 ;

25  PAUSE 0

30  PRINT CHR$ 9 ; “R” ;

35  PAUSE 0

40  PRINT CHR$ 8 ; CHR$ 8 ; “L” ;

45  PAUSE 0

50  PRINT CHR$ 9 ; CHR$ 11 ; “U” ;

55  PAUSE 0

60  PRINT CHR$ 10 ; CHR$ 10 ; “D” ;

65  PAUSE 0

70  PRINT CHR$ 13 ; “ENTER”

75  PAUSE 0

Lo que sí resulta práctico incluir en el sprite son códigos de control de color, para que se imprima con los atributos que deseemos sin tener que usar INK, PAPER, etc., dentro de la sentencia PRINT, lo que, además de ocupar bastante espacio, ejecutaría más lentamente. Esto tiene el inconveniente de estropear la legibilidad del código fuente cuando éste se visualiza mediante el editor de código original del ZX, sin embargo.

Un caso particular de este tipo de ahorro es cuando tenemos que imprimir una secuencia de espacios contiguos en alguna posición de pantalla: si éstos son más de 3, es conveniente utilizar el carácter de control TAB, que automáticamente imprime espacios hasta alcanzar la columna indicada por su argumento. La herramienta de análisis de ZX-Basicus (-a) puede ayudar con esto porque localiza la lista de literales de texto con espacios.

También puede servir el carácter de control COMMA, que inserta espacios hasta alcanzar la siguiente mitad horizontal de la pantalla.

Por supuesto, no se debe borrar toda la pantalla si no es con CLS, o rellenarla si no es con un sólo literal de texto de 32*24 caracteres de longitud (es decir, que ocupe todo el área visible). Éste puede estar optimizado usando TAB o COMMA.

Leer

En cuanto a examinar el contenido de la pantalla con ATTR y SCREEN$, la verdad es que sus implementaciones en la ROM son todo lo eficientes que pueden ser, por lo que, siempre que sus argumentos no sean expresiones de gran complejidad, no son demasiado acelerables.

Se puede usar ATTR preferentemente, que sí es algo más rápida porque no tiene que reconocer el carácter que se examina a partir de su bitmap en pantalla.

Crear UDGs

La creación de gráficos definidos por el usuario (UDGs), si se hace mediante lectura de sentencias DATA en lugar de cargarlos de cinta, es bastante lenta (y, si se hace desde cinta, también ;P). Sólo crear 10 caracteres de este tipo mediante un bucle FOR tarda alrededor de 10 segundos, debidos al propio bucle, a la lectura de los DATA y a que tendremos que actualizar un puntero a memoria donde hacer POKE con cada dato, lo que implica evaluar una expresión.

No es fácil mejorar esto, pero sí se puede hacer que la espera no sea insufrible para el usuario con una pequeña estrategia:

En cada iteración del bucle FOR se puede aprovechar para mostrar en pantalla parte de la ayuda del programa, o cualquier otro elemento que mantenga la atención lejos de la espera. Esta técnica resulta bastante efectiva en la práctica, y sencilla de implementar si la ayuda o el elemento que se ponga en pantalla se lee de los mismos DATA.

Medir el tiempo de ejecución

Ya que estamos hablando mucho en esta serie de la eficiencia en tiempo, es conveniente referirnos a la medida del mismo. El ZX no dispone de ninguna instrumentación en el intérprete para averiguar cuánto ha tardado una sentencia en ejecutar, por lo que sólo nos queda leer la cuenta de “frames” que, gracias a la ULA, se mantiene en la variable del sistema FRAMES. Esta cuenta comienza al arrancar el ordenador (al hacer un reset) y aumenta en 1 unidad por cada 20 milisegundos en los ZX europeos.

El problema que tiene acceder a esta cuenta para medir tiempos es que ese acceso no se puede hacer demasiado eficientemente en BASIC. La forma más rápida sería la expresión 65536 * PEEK 23674 + 256 * PEEK 23673 + PEEK 23672, la cual enlentece bastante de por sí al programa debido a las multiplicaciones y PEEKs, es decir, su complejidad es alta. Se puede acelerar algo si sabemos que los tiempos que debemos medir serán siempre menores de 65536 * 20 milisegundos (algo menos de 22 minutos). En este caso podemos hacer POKE con 0 en los tres bytes de la variable antes de comenzar a contar, y luego leer sólo los dos menos significativos. En caso de querer acelerar aún más tendríamos que asegurarnos de que los tiempos van a ser inferiores o iguales a 5 segundos; entonces podemos ignorar los dos bytes más significativos de la variable de igual forma, y quedarnos sólo con PEEK 23672.

A pesar de estas limitaciones, si se dispone de un buen emulador del ZX Spectrum que permita escribir en la ROM (algo imposible en un ZX real), se podría medir el tiempo que tarda una sección del código BASIC mediante la instrumentación de dicha ROM y un programa en ensamblador que extendiera su funcionalidad original.

Más concretamente, la idea básica sería intervenir la rutina de la ROM encargada de ejecutar sentencias individuales, llamada THE STATEMENT LOOP y ubicada en la dirección 0x1b28 de la ROM. Justo 5 bytes después, en la dirección 0x1b32, comienza la interpretación de la sentencia en curso, cuyo número de línea se encontrará en ese momento en la variable del sistema PPC y cuyo número de sentencia estará en SUBPPC. Por tanto, si podemos escribir en la ROM y sustituir 3 bytes de la dirección 0x1b32 por una llamada a una rutina externa, llamada que ocupa exactamente 3 bytes de código máquina (C/M), y si esa rutina externa toma nota del tiempo actual si comprueba que el programa entra en o sale de la sección que queremos medir, es posible almacenar dichas medidas para sacar luego estadísticas de la duración de su ejecución.

Esto precisamente es lo que hace la herramienta que se puede obtener aquí mismo. Su uso es un poco complicado, y no se recomienda para quien no tenga ya cierta experiencia en la programación del ZX Spectrum.

Se trata de un programa en C/M que ha de cargarse, después del programa BASIC a medir, en la dirección 60000 de memoria (debe haberse hecho un CLEAR previamente a alguna dirección inferior; este C/M respeta los UDGs). También debe permitirse en el emulador la escritura sobre la ROM, como hemos dicho antes.

Una vez cargado este C/M, hay que: a) inicializar una tabla de datos para el mismo, así como crear algunas variables (todo esto se muestra en el ejemplo a continuación); b) definir el trozo de programa que se quiere medir, desde la primera sentencia a medir hasta la primera a no medir, mediante la creación de más variables y su almacenamiento en la tabla de datos; c) almacenar en la tabla el número n de medidas de tiempo a tomar, que debe ser menor o igual a 1000, y activar el proceso de medida mediante RANDOMIZE USR 60000; d) dejar que se ejecute el programa BASIC mientras el medidor C/M almacena medidas de tiempo tomadas cada vez que se ejecuta la sección definida (si ésta se repite, se tomará más de una medida; si no, sólo una); e) cuando se desee dejar de medir, restaurar la ROM original haciendo RANDOMIZE USR 60038; f) parar el programa BASIC y almacenar los datos obtenidos leyéndolos de memoria del emulador.

Un ejemplo de uso es el siguiente, en el que se mide lo que tarda la impresión en pantalla de la zona sombreada (línea 10, sentencia 2; las sentencias se numeran desde 1), que calcula el coseno de una variable entera:

CLEAR 59999 : GOSUB 9999 : REM load machine code and prepare table

LET firstline = 10 : LET firststat = 2 : LET endline = 10 : LET endstat = 3 : POKE postable + 6 , FN l ( n ) : POKE postable + 7 , FN h ( n ) : POKE postable , FN l ( firstline ) : POKE postable + 1 , FN h ( firstline ) : POKE postable + 2 , firststat : POKE postable + 3 , FN l ( endline ) : POKE postable + 4 , FN h ( endline ) : POKE postable + 5 , endstat : RANDOMIZE USR 60000 : REM activate

10  FOR f = 1 TO 1000 : PRINT AT 0 , 0 ; COS ( f ) : NEXT f

20  RANDOMIZE USR 60038 : REM deactivate

30  STOP

9999  LOAD “” CODE 60000 : DEFFN h ( w ) = INT ( w / 256 ) : DEFFN l ( w ) = w INT ( w / 256 ) * 256 : LET n = 1000 : LET tam = n * 5 + 15 : LET postable = 65536 ( 21 * 8 ) tam : POKE postable + 11 , FN l ( postable + 15 ) : POKE postable + 12 , FN h ( postable + 15 ) : POKE postable + 13 , FN l ( 60054 ) : POKE postable + 14 , FN h ( 60054 ) : POKE 23728 , FN l ( postable ) : POKE 23729 , FN h ( postable ) : RETURN

La línea 9999 se encarga de cargar el C/M y crear la tabla de datos necesaria para éste (paso a). Nótese que para que funcione bien, este programa BASIC no debe tener funciones de usuario llamadas FN h o FN l. Los pasos b y c se implementan en la línea 2, el d es la línea 10, y el e la 20.

El ejemplo de arriba produce 1000 medidas de tiempo (las veces que se repite el bucle FOR, y el máximo que el programa en C/M es capaz de tomar), que se almacenan a partir de la dirección 60368 de la RAM, como se muestra en el visor de memoria del emulador fuse después de terminar el experimento:

Cada medida de tiempo son 5 bytes (la primera que se muestra en la figura de arriba es 03 00 00 60 04): los 3 primeros contienen un valor f igual a la diferencia de la variable del sistema FRAMES al principio y al final de la sección de código monitorizado; los 2 siguientes contienen un valor entero c que afina esa medida de tiempo. El tiempo total transcurrido, eliminando el propio tiempo incurrido por la rutina externa de C/M, puede calcularse a partir de f y c con la siguiente fórmula, que está diseñada específicamente para la actual implementación de dicha rutina:

P_{ULA} - (35 \cdot c + 30) P_{CPU} + f \cdot P_{ULA} -(323+192)P_{CPU}

donde PULA es el período de refresco de la ULA (20 milisegundos en Europa) y PCPU el período de reloj de la CPU (286 nanosegundos si ésta funciona a 3.5MHz). Esta fórmula no es del todo exacta porque el valor 30 del segundo término puede estar en realidad en el intervalo [0,30] y porque puede ocurrir una interrupción de la ULA dentro de los 192 T-estados que aparecen en el último término, aumentándolos en lo que servir esa interrupción tarde (que dependerá del valor de la variable FRAMES y del estado del teclado, fundamentalmente). Sin embargo, la probabilidad de que ocurra dicha interrupción en ese período es de 192*PCPU/0.02 = 0.0027, que es despreciable.

Los datos obtenidos de este ejemplo se han grabado en cinta desde el propio intérprete de BASIC (SAVE), se han extraído del fichero de cinta con un visor de datos binario para Linux (Bless), y se han interpretado con un script de Matlab (se podría haber hecho igualmente con cualquier otro lenguaje de scripting, como Python) según la fórmula anterior para obtener estadísticas de los mismos. Los resultados se muestran en la siguiente figura:

Como se puede observar, la lentitud del intérprete BASIC del ZX Spectrum puede ser exasperante… ¡70 milisegundos para calcular y mostrar en pantalla el coseno de una variable entera! Le daría para imprimir solamente 14 en un segundo…

.oOo.


[Click here to read this in Spanish ]

This is the fourth and last one in a series of posts that explain the foundations of the (in)efficiency of pure BASIC programs written for the ZX Spectrum:

I. On line numbers

II. On variables

III. On expressions

IV. Statements and time measurements

In this last post we talk about the efficiency of some important functionality in the ZX Spectrum BASIC and about time measurements.

Drawing

The drawing statements (PLOT, DRAW, CIRCLE) are specially sensitive regarding their execution speed, as anyone that has programmed sometime in the ZX knows well. When DRAW is used to draw arcs, and in all uses of CIRCLE, the interpreter does repeated floating point calculations, which makes them very slow in spite of having optimized ROM implementations. PLOT is not inefficient in principle (particularly if its arguments are integer expressions and do not use reals), but to draw something interesting only with PLOT you need a lot of plots!

Drawing straight lines with DRAW uses a version of the Bresenham’s algorithm which is quite efficient (it does not need to use the ROM calculator), but, like other drawing routines, it must to set appropriately the color attributes of the screen cells which the line goes through, besides executing the INVERSE or OVER modes correctly. That reduces its efficiency.

In summary:

Drawing statements (and bitmap consulting statements, like POINT) should be used sporadically, with integer arguments, in parts of the code that do not have special speed requirements, and preferably for straight drawings.

Printing (on Screen)

Printing stuff on screen is essential in any ZX program. In the case of printing sprites, i.e., a contiguous set of characters with certain width and height (usually composed of UDGs), it is not convenient to use FOR to scan and print every character due to the extra computation time and program space in memory; instead, you may think of including as part of the characters of the sprite some that control the printing position.

Alas, unfortunately the interpreter does not implement correctly the control characters in charge of directional movements (arrows). For coping with the lack of directional control characters we can rely on loop unrolling techniques, that we explained in previous posts.

This short program illustrates some of the problems of the interpreter to use the directional control characters:

10  BORDER 1 : CLS

20  PRINT AT 10 , 10 ; PAPER 5 ; “0” ; PAPER 7 ;

25  PAUSE 0

30  PRINT CHR$ 9 ; “R” ;

35  PAUSE 0

40  PRINT CHR$ 8 ; CHR$ 8 ; “L” ;

45  PAUSE 0

50  PRINT CHR$ 9 ; CHR$ 11 ; “U” ;

55  PAUSE 0

60  PRINT CHR$ 10 ; CHR$ 10 ; “D” ;

65  PAUSE 0

70  PRINT CHR$ 13 ; “ENTER”

75  PAUSE 0

There is however a practical use of control characters when printing sprites: color control (and bright, flash, etc.). With them we can avoid the use of the INK, PAPER, etc. statements, which are much slower to run and require to evaluate additional expressions (their arguments). Also, we save program memory space. The drawback is that the listing of the program code gets messed up when seeing in the ZX editor, but that has no effect during execution.

Printing can also be made more efficient when we have to print a sequence of spaces. If they are more than 3, it is convenient to use the TAB control character instead, which automatically prints spaces until reaching the column given in its argument (the control char plus the argument are 2 bytes only and no expression evaluation is involved). The ZX-Basicus analysis tool (-a) can help in this sense since it locates the text literals that contain sequences of spaces.

Also, sequences of spaces can be substituted by the COMMA control character, but that is less flexible: it prints spaces until reaching the next half of the screen.

Of course, you should not print spaces individually to clear the screen, but use CLS, or, alternatively, print a number of COMMA control characters that fill it.

Reading the Screen

In games it is very useful to read some screen position and tell which character is printed there, or which attribute is used there; in that way, the program can decide whether there has been a collision between sprites, for example.

Both ATTR and SCREEN$ functions are quite efficiently implemented in ROM, thus they cannot be greatly accelerated beyond using integer literals in their arguments.

ATTR is faster, though, since it does not interpret the bitmap to deduce the character, thus it should be the preferred collision-detection method.

Creating UDGs

Creating UDGs means to fill a certain area of memory with the bitmaps of those UDGs. If this is done by reading DATA values and POKE-ing them, it is quite slow. Just creating 10 UDGs with a FOR loop takes around 10 seconds, due to the loop itself (consulting the FOR variable, jumping in NEXT…), reading the DATA (locating the line where the DATA is, recall the first post in this series), and maintaining the pointer to memory that holds the address where to POKE the next byte.

It is not easy to improve this (if you load the UDGs from tape, the loading time is not much better). However, there is a technique that can hide that time for the user, making him/her to be mentally active in the meanwhile instead of doing a simple wait till completion:

In each iteration of the loop that is defining the UDGs, you can show in the screen part of information needed to use your program, or any element that distracts the attention of the user. It is amazing how this simple strategy can make the wait more pleasant and even dissapear in the conscience of the viewer! It is also quite simple to implement; you even can use the same DATA that hold the UDG bitmaps to hold the information to display, thus READing it on the fly.

Measuring time

Besides making programs time-efficient, many times you need to take notice of the time spent in some part of your code. The ROM interpreter is not instrumented to measure the time spent in any statement, but we have the system variable FRAMES, that, thanks to the ULA, keeps a count of the 20 millisecs ticks (16.7 millisecs in non-european countries) that have passed since the computer was reset the last time.

The problem with reading this variable is that it is a long number formed by 3 bytes. Getting the value is slow even using the most direct expression for it: 65536 * PEEK 23674 + 256 * PEEK 23673 + PEEK 23672. There are a bunch of multiplications, sums and memory readings there. It can be simplified if we know that the times to measure are always shorter than 65536 * 20 millisecs (around 22 minutes). In that case, we can POKE a 0 in the three bytes of the variable before starting to count, and then reading only the 2 least significan bytes. Moreover, we can use just the least significant byte if all times to measure will be shorter than 5 seconds: the previous expression becomes then PEEK 23672.

In spite of these limitations, we could think of measuring the time spent in a section of a BASIC program if we use a good emulator that allows us to do writings at ROM addresses (something imposible with a real ZX Spectrum!). That ROM could be instrumented and extended with some assembly code to gather the measurements.

More concretely, the basic idea is to change the original ROM routine that is in charge of interpreting and executing individual BASIC statements, called THE STATEMENT LOOP and placed at 0x1b28 within the ROM. Right 5 bytes after that, at 0x1b32, the code that interprets the current statement starts. When that happens, the line number of that statement is in the system variable PPC, and the statement number in SUBPPC. Consequently, if we could write at that ROM address in order to substitute 3 bytes at 0x1b32 by a call to an external machine code (M/C) routine (such a call are exactly 3 bytes) which timestamps the current time if the BASIC program is entering or leaving the section to measure, it would be possible to store those measurements in order to do an offline statistical analysis.

This is precisely what the tool that you can download here does. Notice that this tool is not for beginners; it is only recommended if you have experience in ZX Spectrum programming.

The tool is a M/C program that must be loaded in memory after the BASIC program that we wish to measure is loaded. The M/C must be loaded concretely at address 60000 (CLEAR must has been done previously to a smaller address; the M/C program respects the UDGs); in the emulator, you must allow for ROM writings, as we have commented.

Once the M/C program is loaded, you have to: a) initialize a data table for it, and create some variables (all of that is shown in the example below); b) define the section of BASIC code you wish to measure, from the first statement to be measured to the first statement NOT to be measured, and store in the data table that information; c) store in the data table the number n of measurements to accumulate on that section, that must be smaller than or equal to 1000, and activate the measurement procedure through RANDOMIZE USR 60000; d) leave the BASIC program to run while the monitoring M/C program gathers one measurement each time the monitored section executes (if the section executes more than once, several measurements are gathered); e) when you wish to stop measuring, restore the original ROM with RANDOMIZE USR 60038; f) stop the BASIC program and gather the measurements for offline analysis, for instance in a tape file.

A case use is the following, where we measure how long takes to calculate and print in the screen the cosine of an integer variable (line 10, statement 2 in the code; statements are numbered from 1):

CLEAR 59999 : GOSUB 9999 : REM load machine code and prepare table

LET firstline = 10 : LET firststat = 2 : LET endline = 10 : LET endstat = 3 : POKE postable + 6 , FN l ( n ) : POKE postable + 7 , FN h ( n ) : POKE postable , FN l ( firstline ) : POKE postable + 1 , FN h ( firstline ) : POKE postable + 2 , firststat : POKE postable + 3 , FN l ( endline ) : POKE postable + 4 , FN h ( endline ) : POKE postable + 5 , endstat : RANDOMIZE USR 60000 : REM activate

10  FOR f = 1 TO 1000 : PRINT AT 0 , 0 ; COS ( f ) : NEXT f

20  RANDOMIZE USR 60038 : REM deactivate

30  STOP

9999  LOAD “” CODE 60000 : DEFFN h ( w ) = INT ( w / 256 ) : DEFFN l ( w ) = w INT ( w / 256 ) * 256 : LET n = 1000 : LET tam = n * 5 + 15 : LET postable = 65536 ( 21 * 8 ) tam : POKE postable + 11 , FN l ( postable + 15 ) : POKE postable + 12 , FN h ( postable + 15 ) : POKE postable + 13 , FN l ( 60054 ) : POKE postable + 14 , FN h ( 60054 ) : POKE 23728 , FN l ( postable ) : POKE 23729 , FN h ( postable ) : RETURN

Line 9999 loads the M/C program and create the data table for it (step a). Notice that in this example, the BASIC program cannot use user functions FN h or FN l. Steps b and c are implemented in line 2, d in line 10, and e in line 20.

The above example produces 1000 measurements (the number of times that the FOR loop iterates, and the maximum to gather by the M/C program), that are stored from address 60368 up, as it is shown in the memory browser of the fuse emulator after finishing the experiment:

Each measurement consists of 5 bytes (the first one shown above is 03 00 00 60 04): the first 3 bytes are an integer value f that equals the increment of the FRAMES system variable since the starting of the monitored section to its end; the next 2 bytes contain an integer value c that refines that measurement. The total time spent in the monitored BASIC section, once the very time of the M/C routine is discarded, can be calculated from both values using the following formula, designed for the particular implementation of the M/C routine:

P_{ULA} - (35 \cdot c + 30) P_{CPU} + f \cdot P_{ULA} -(323+192)P_{CPU}

where PULA is the refresh period of the ULA (20 milliseconds in Europe) and PCPU the clock period of the CPU (286 nanoseconds if it is a 48K@3.5MHz). This formula is not entirely exact since the value 30 in the second term can actually be within the interval [0,30] and because one interruption of the ULA may occur within the 192 T-states that are in the last term, increasing them by what the ULA service routine takes (which depens on the value of the FRAMES system variable and the state of the keyboard, essentially). However, the probability of that interrupt to occur in such a short interval is 192*PCPU/0.02 = 0.0027, which is negligible.

The data gathered from our example have been saved in a tape file (SAVE), have been extracted from that file with a binary viewer in Linux (Bless), and have been interpreted with a Matlab script (you could use any other scripting language, such as Python) according to the formula above. The statistical results are as follows:

As you can see, the BASIC interpreter of the ROM is exasperatingly slow… 70 milliseconds to calculate and print the cosine of an integer variable! It could only print 14 of these variables in one second…

Facebooktwitterredditlinkedintumblrmail