cursos:ensamblador:avanzadas2

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


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



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.


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


[ | | ]

  • cursos/ensamblador/avanzadas2.txt
  • Última modificación: 24-01-2024 17:25
  • por sromero