miércoles, 31 de mayo de 2017

Branchs para andar a los saltos

Quien aprende a programar en alto nivel puede que sienta que su vida se complica cuando halla que en los bajos fondos del assembler no hay if, case/switch, for, while... la carencia de estructuras de control que formaron parte desde un mismo comienzo de su aprendizaje de la programación. Ahora bien, que no existan las mencionadas estructuras no significa que no las podamos imitar. La verdad es que a base de saltos o bifurcaciones nos las podemos arreglar para extrañar menos a if y sus amigas. Veamos cómo emplear la familia de instrucciones branch y conozcamos mejor a cada integrante del clan. 
No voy a entrar en detalles sobre el cálculo de los saltos, dado que en la nota Direccionamiento (continuacion). Todo es relativo... bah, no siempre. está cubierto. Sugiero leer primero la serie de dos partes sobre Direccionamiento antes de abordar el presente tema. Adicionalmente hallará más información sobre aplicación de saltos a comparaciones rutinarias en la serie de cuatro partes ¿Ser o no ser?
Conozcamos a la familia
Toda las instrucciones de bifurcación/salto condicional las encontraremos agrupadas en el set de instrucciones en la "B", y salvo un par de excepciones podríamos decir que si empieza con B es Branch. (Lamento(?) si la mención de la segunda letra del abecedario trae algún recuerdo doloroso para simpatizantes de algún club de fútbol. El uso de los colores rojo y blanco en los gráficos es totalmente casual.). 
Observaremos que todos los branchs en su columna "Descripción" tienen un signo de interrogación y la condición que evalúan para determinar si se ejecuta el salto o no. A excepción de BRCLR y BRSET, a las que abordaremos en forma particular, todos los saltos dependen del resultado algebraico booleano obtenido al operar con banderas de la palabra de estado.

El signo de interrogación es otra forma de indicar que el salto se produce solo si la respuesta a la pregunta es afirmativa. Siempre se trata de condiciones booleanas, por tanto la suma es en realidad una operación OR booleana (basta con que uno de los términos sea verdad para que la proposición sea verdad), el producto es AND booleano (ambos términos han de ser verdad para que la proposición también los sea) y el más dentro de un círculo es XOR (uno y solo uno de los dos términos debe ser verdad para que la proposición lo sea). Incluso en BRSET se usa un negador sobre M, que nuevamente debemos interpretar como NOT booleano.

Entendiendo a cada integrante, del derecho y del revés
Hay más de una manera de escoger el miembro de la familia Branch correcto para nuestro algoritmo. Por ejemplo, podemos interpretar fácilmente cuál es la condición que evalúa BCC y BCS. Si al elaborar el algoritmo requerimos de un salto por el estado del flag de Carry, es más sencillo que nuestra mente nos lleve a usar BCC (salte si carry está en cero, branch if carry clear) o BCS (salte si carry está en uno, branch if carry set).
Si por el contrario estamos verificando la condición de cero nos quedaremos con BEQ y BNE, que saltan cuando Z (cero) es 1 o no. Lo mismo podríamos aplicar a BPL y BMI, que operan sobre el flag N (negativo).
El problema radica en entender ¿por qué me tengo que fijar en los flags (N,Z,V,C)? El significado de los flags para cada operación se desarrolla en un post aparte, pero veamos algunas particularidades.

Compare por ejemplo estos dos pares de instrucciones:  BCS, BCC y BLO, BHS. 
Estas cuatro instrucciones -que realmente son dos y sus alter ego- son un claro ejemplo de las dos formas de entender cada una.
El asunto es ¿qué significa que carry esté en cero o uno, en verdadero o falso? Dependerá de la operación previa, si antes del branch ejecutamos una suma entonces el significado de Carry será el acarreo de dicha operación. ¿Y si ejecutamos una resta o una comparación? Recordemos que una comparación no es otra cosa que una resta de la que no nos interesa el resultado. ¿Qué significado tiene Carry en estos casos?
Remontémonos por un momento a los años felices en que comenzábamos a descubrir el mundo de las matemáticas. Lejos de integrales, límites, diferenciales y tantas cosas que luego quemaron nuestros cerebros, descubríamos inocentes una operación que sigue acompañándonos a diario (especialmente luego de cobrar el sueldo): la resta. ¿Qué ocurría cuando teníamos que restar y no nos alcanzaba? Por ejemplo, si la resta era:
12
- 9
No podíamos restar 9 de 2... así que le "pedíamos al compañero". Ese pedido/préstamo (en inglés borrow) nos permitía resolver la resta. Lo mismo aplicaba a restar a un número otro mayor, por ejemplo restarle 15 a 9 nos generaba el mismo borrow, solo que aquí se lo aplicamos al signo del número haciendo que el resultado sea negativo.
Por lo tanto, si luego de una comparación de números sin signo deseamos saber cuál de los dos es mayor... ¿qué flag podemos evaluar? (por favor observe que esto es válido para números sin signo!, luego desarrollaremos los signados) En este momento puede que revise la composición de la palabra de estado y no encuentre ninguna "B" (alivio para un club afecto al maíz)... ¿entonces? Resulta que el mismo flag que se usa para Carry es el que se emplea para Borrow. De hecho en los manuales del HC11 aparece como Carry/Borrow en más de una ocasión.
La respuesta es: Carry. Si el segundo número de la comparación es mayor, entonces Carry/Borrow será 1 porque "no le alcanzó" al primero para la resta. Recordemos que la comparación es una resta que no se resuelve sino solo para los flags.
Del mismo modo, si tenemos dos números -signados o no signados, es lo mismo- y queremos saber si son iguales... ¿cómo lo podemos determinar? Si se restan dos números iguales el resultado es cero. En cualquier otro caso el resultado es distinto de cero. Eso significa que podemos usar el flag Z (cero) y los saltos asociados BEQ y BNE.
En otro post desarrollaré con más detalle cómo tomar decisiones con números signados. Veamos el segundo método para escoger saltos.

Menos filosofía y cuentitas y más programación.
Una manera más pragmática de determinar el branch a emplear es recordando que el HC11 solo permite comparaciones entre registros o entre registro y memoria. Por ejemplo, si queremos comparar dos direcciones de memoria (digamos $0010 y $0011) podríamos hacer:

LDAA $0010
CMPA $0011 ; obviamente comparo el contenido de las direcciones

otra alternativa sería
LDAA $0010
LDAB $0011
CBA        ; algo menos eficiente, usé una instrucción adicional

Observe en el set de instrucciones la columna "Description" de las instrucciones de comparación (hice un collage para que esten todas juntas):
¿Qué puede ver en común en todas ellas? Siempre son restas y nunca se asignan los resultados (o sea el resultado se descarta, no se puede obtener con ellas). También note que (salvo CBA) todas comparan un registro (A, B, D, IX, IY) contra memoria. En la resta siempre va primero el registro y segunda la memoria. En el caso de CBA la operación es A-B, para lo que vamos a definir ahora "B" hace las veces de memoria. (Parece a propósito que saco a colación la "B" y la memoria, pero no, para nada). Recordando el orden de la comparación (registro respecto a memoria) veamos ahora esta tabla:
BHS
Branch if Higher or Same
r >= M
No signado
BLO
Branch if Lower
r < M
No signado
BHI
Branch if Higher
r > M
No signado
BLS
Branch if Lower or Same
r <= M
No signado
BLE
Branch if Less or Equal
r <= M
Signado
BGT
Branch if Greater Than
r > M
Signado
BGE
Branch if Greater or Equal
r >= M
Signado
BLT
Branch if Less Than
r < M
Signado

Supongo que el poco de inglés que se necesita a este punto ya lo comprenderá. Observe la correlación entre el mnemónico (expandido en la segunda columna) y el signo de desigualdad de la tercera.

A este punto debiera ser más clara la decisión. Si estamos comparando dos valores y deseamos que se cambie el flujo de programa (o sea se produzca un salto o bifurcación, branch) la condición por la que deseamos saltar será la que se lea en "inglés natural" si recordamos que estamos comparando registro contra memoria (en ese orden).

El único truco adicional es saber qué salto utilizar si estamos tratando con números signados o no, y para eso va el comentario de la cuarta columna.

Habrá segunda parte para que conozcamos sobre BRSET y BRCLR, BRA y BRN y para que tratemos de asimilar la forma de decidir saltos pensando en bajo nivel.

Me despido con un clásico del Borrow...



3 comentarios:

  1. Hola profe, primero que nada tengo que felicitarlo y agradecerle por el blog. Llevo haciendo los ejercicios que propone aquí y por suerte estoy entendiendo. Pero en este tema todavía hay cosas que no entiendo. Por ejemplo cuando comparo si es menor (BLO), solo veo el Carry porque ambos serían no signados entonces se activaría para un borrow. Ahora entre BLT y BMI en ambos veo si es menor a 0 (negativo). En BMI solo importa si tiene signo negativo. ahora en BLT porque veo el overflow para ver si es menor? Esos detalles me pierden porque para ver si es menor a cero solo tendría que ver el flaco N. Porque el overflow?
    Hernán t.mañana mier-vie.

    ResponderBorrar
  2. Hola Hernán! Gracias!
    BMI salta cuando el flag N se activa. Ese flag copia el MSB del resultado de la última operación de la ALU (resultado que puede quedar en un registro o no). BMI te permite saltar si el resultado tiene el MSB en 1. Eso puede significar (bajo ciertas condiciones) que el resultado es negativo.
    En cambio BLT verifica otros flags, porque responde a la comparación "menor que" (Branch if Less Than) y es el que realmente debemos usar cuando comparamos dos números signados y queremos que ejecute el branch cuando el segundo es menor al primero. Sugerencia: en el simulador ejecutá el ordenamiento de vectores (burbujeo es el más pavo y fácil de seguir para esto) y fijate cómo se comporta con BLT. Nos vemos!

    ResponderBorrar
    Respuestas
    1. Ah buenísimo, ahí lo probé a ambos (BLT y BLO) y entendí mejor la diferencia. Muchas gracias!!
      Hernán.

      Borrar

Aunque no es obligatorio, es de buena educación firmar los comentarios.