Consideraciones avanzadas (II)
En este capítulo vamos a ver algunas consideraciones relativas a optimizaciones y mejoras que no hemos querido incluír dentro de sus respectivos capítulos para evitar confundir al lector con código o ideas complejas en un momento en que está aprendiendo los fundamenos..
Optimizaciones generales
Veamos una serie de “optimizaciones” o construcciones de código algo más avanzadas que las que hemos visto hasta ahora para determinadas tareas:
Copiar el flag Zero al Carry
scf ; CF = 1 jr z, $+3 ; Si ZF=1, saltamos y dejamos CF=1 ccf ; Si ZF=0, al no saltar ejecutamos ccf ; y hacemos CF=0 ; $+3 es la instrucción posterior al ccf
Copiar el flag de Carry al Zero flag
ccf sbc a, a
Realizar un NEG
de un registro de 16 bits
xor a sub Parte_baja_Registro ld Parte_baja_Registro, a sbc a, a sub Parte_alta_Registro ld Parte_alta_Registro, a
Por ejemplo, para simular NEG HL
:
xor a sub l ld l, a sbc a, a sub h ld h, a
Optimizaciones de bucles
Bucles de 16 bits optimizados
Es posible hacer loops de 16 bits que son tan rápidos como los de 8, tal y como se explica en la siguiente URL
La forma normal de realizar loops de 16 bits sería algo como:
ld de, NNNN ; veces a iterar loop: ; ... código de nuestro bucle ... dec de ld a, d or e jp nz, loop
El problema es que tenemos que hacer operaciones costosas como el dec de
(de 16 bits), perder el valor de A para poder comprobar si hemos llegado al final del bucle.
Para eso, esta página nos propone aprovechar DJNZ
. Cuando usamos DJNZ
, el valor de B se decrementa hasta que alcanza 0. La comprobación de si el bucle ha acabado (si B == 0) se hace después del decremento, por lo que podemos aprovechar esto para hacer un bucle de 256 iteraciones si ponemos B a 0.
Poniendo B a 0, tras ejecutar el código 1 vez, se decrementa B, con lo que pasa a valer $ff (255), así que nos aseguramos 255 ejecuciones más la inicial, total 256.
Debido a esto, podemos hacer algo como lo siguiente: partimos el número de 16 bits a iterar en 2 registros e iteramos el primero un total de veces almacenado en el segundo. Cada vez que el primer registro llegue a cero, volveremos a iterar 256 veces, salvo la primera vez que iteramos el “módulo” de 256.
Ejemplo:
ld b, 10 ; loop LSB ld d, 3 ; loop MSB => $050a => 522 iteraciones loop: ; ... código de nuestro bucle ... djnz loop dec d jp nz, loop
Este bucle iterará un total de 522 veces ($050a).
Ahora la pregunta es ¿cómo calculamos los valores que tenemos que poner en D y en B?
Con el siguiente código:
ld de, NNNN ; veces a iterar ld b, e dec de inc d ; D y B listos para usar en nuestro bucle
Es decir, el bucle completo sería:
ld de, NNNN ; veces a iterar ld b, e ; dec de ; Calculate DB value (destroys B, D and E) inc d loop: ; ... código de nuestro bucle ... djnz LOOP dec d jp nz, LOOP
Si queremos usar B y C en lugar de D y B:
ld de, NNNN ; veces a iterar ld b, e ; dec de ; Calculate DB value (destroys B, D and E) inc d ld c, d loop: ; ... código de nuestro bucle ... djnz LOOP dec c jp nz, LOOP
Al respecto de los tiempos:
El bucle de 16 bits estándar que hemos visto en el primer ejemplo utiliza 4 instrucciones para hacer el bucle, lo que suma un total de 28 ciclos de reloj o T-states por iteración.
En cambio, el bucle con DJNZ
+ jp
(o jr
si el código del bucle es menor de 128 bytes) utiliza DJNZ
, que son sólo 14 ciclos de reloj, es decir la mitad. Sólo cada 256 iteraciones se ejecutará el bucle exterior utilizando un total de 25 ciclos de reloj.
El bucle exterior se ejecuta de forma poco frecuente (1 vez cada 256 iteraciones), por lo que estamos ahorrando 14 ciclos en cada iteración y perdiendo uno cada 256.
Hemos necesitado 25 ciclos de reloj para precalcular inicialmente los valores del bucle, pero como vemos, se han recuperado a partir de la tercera iteración del bucle si lo comparamos con un bucle de 16 bits normal.
Uso de los Shadow Registers
Cuando explicamos los Shadow Registers, dijimos que no eran excesivamente aprovechables puesto que no podíamos acceder a ellos directamente ni podían convivir junto a los registros normales, por lo que quedaban relegados a su uso en ISRs o rutinas “finales” que no tuvieran parámetros ni devolvieran valores, o que lo hicieran a través de variables en RAM. También vimos que es cierto que hay determinados casos donde sí podemos aprovechar los 2 pares de registros a la vez, y es usando la pila. Por ejemplo, podríamos trabajar con todos los pares de registros utilizando después PUSH
en uno de ellos y después de un EXX
un POP
para recuperarlo donde nos convenga.
Un ejemplo de uso de los registros alternativos es multiplicar vectores 16 bits por escalares, aritmética de punto flotante, como se puede ver en la siguiente URL:
Finalmente, un caso muy interesante del uso de ex af, af'
es el de preservar AF en alguna operación, para poder aprovechar ese flag después.
Por ejemplo, supongamos que hacemos un scroll de un bitmap así:
rl (hl) dec l ; este bloque RL + DEC se repite 16 veces
En este caso, rotamos el byte apuntado por HL a la izquierda, de forma que el Carry Flag entra por la derecha con el nuevo pixel entrante en la pantalla.
¿Qué ocurre si queremos scrollear 2 pixeles, y necesitamos no perder el Carry Flag del primer RL cuando hacemos el segundo? Pues que podemos preservarlo dejándolo en AF' y recuperándolo después:
rl (hl) ex af, af' ; Nos guardamos AF en AF' rl (hl) ex af, af' ; Recuperamos AF desde AF' dec l ; este bloque se repite 16 veces