Está en la página 1de 39

Capítulo 5

Procesamiento VLIW

Los procesadores VLIW (Very Long Instruction Word) son procesadores segmentados que
pretenden aprovechar el paralelismo entre instrucciones (ILP) alcanzando valores del CPI
(ciclos por instrucción) por debajo de la unidad. Sin embargo, mientras que en un procesador
superescalar se incluyen recursos hardware como los búferes de reordenamiento, los búferes
de renombrado, etc., para extraer dinámicamente el paralelismo, en un procesador VLIW, el
compilador es el responsable fundamental a la hora de aprovechar de forma óptima el
paralelismo del procesador. De esta forma, se pretende reducir la complejidad hardware de la
microarquitectura utilizando cauces más escalables, en los que el aumento en el número de
instrucciones que se emiten por ciclo no suponga un incremento demasiado elevado de la
complejidad del mismo, y con un consumo energético moderado por parte del procesador.

Captación Cola de
(IF) instrucciones

Decodificación
(ID)

Slot de emisión Slot de emisión Slot de emisión

Unidad Unidad Unidad Ejecución


funcional funcional funcional (EX)

Registros de
la arquitectura

Figura 1. Esquema simplificado de un cauce VLIW.

El término VLIW hace referencia al hecho de que las instrucciones que procesa el cauce las
construye el compilador empaquetando operaciones que pueden ejecutarse en paralelo en el
procesador (debido a las unidades funcionales disponibles y a la independencia entre las
mismas). En un procesador superescalar, cada una de esas operaciones se codificaría
mediante una única instrucción y las dependencias entre ellas se deben comprobar en el
propio cauce. En un procesador VLIW, gracias al trabajo del compilador, las operaciones
empaquetadas en una instrucción VLIW son independientes y pasan a los slots de emisión del
procesador sin más comprobación. Esto simplifica la microarquitectura del procesador
VLIW con respecto al superescalar. Se dice que en un procesador superescalar, la
planificación de instrucciones es dinámica, mientras que en un VLIW es estática. La Figura 1
muestra un esquema de cauce superescalar. Frente a un procesador superescalar, un
procesador VLIW tiene la desventaja de que una menor portabilidad de los códigos de
procesadores de una misma familia, pero con ciertas diferencias en la microarquitectura, ya
que el compilador debe tener en cuenta las características concretas de la microarquitectura
del cauce para la que se esté generando código.

En un procesador VLIW es fundamental que el compilador sea capaz de encontrar


instrucciones que puedan ubicarse en cada uno de los campos de operación de la instrucción
VLIW (también denominados huecos o slots) ya que de esa manera se podría aprovechar el
máximo paralelismo que implementa la máquina. Para eso es necesario encontrar tantas
instrucciones independientes como slots tengan las instrucciones VLIW y además, cada una
de esas operaciones debe poderse ejecutar en las unidades funcionales a las que puede
accederse desde cada slot. En el caso de que existan limitaciones importantes en el acceso a
las unidades funcionales (por ejemplo, que desde cada slot sólo pueda accederse a un único
tipo de unidad funcional), puede resultar complicado para el compilador encontrar
instrucciones independientes para todos los slots, por lo que no se aprovecharía el paralelismo
que ofrece la microarquitectura. Por otra parte, si no se encuentra una operación
independiente para un slot, debe insertarse una operación nop, de forma que siempre están
completos todos los slots de emisión. Estas restricciones pueden llegar a ocasionar códigos de
programa poco densos que necesiten más memoria para almacenarse que los equivalentes
para un procesador superescalar.

Los riesgos de control dificultan la planificación del código al compilador, ya que en tiempo
de compilación no se puede saber, a ciencia cierta, qué comportamiento tendrán los saltos
del programa, por lo que se complicará la tarea de encontrar operaciones independientes para
completar las instrucciones VLIW. Por lo tanto, cuantos menos saltos aparezcan en el
código, más sencilla será la planificación. Se denomina bloque básico al conjunto de
operaciones situadas entre dos instrucciones de salto. Así que la tarea principal del
compilador consistirá en la planificación más adecuada de las operaciones de cada uno de los
bloques básicos del programa. Existen algunas técnicas software que permiten aumentar el
número de instrucciones independientes de los bloques básicos, de forma que se facilite la
planificación local de instrucciones, como el desenrollado de bucles o la segmentación software
(software pipelining).

Para mostrar el funcionamiento de estas dos técnicas utilizaremos el siguiente ejemplo.


Supongamos el siguiente fragmento de código:

for (i = 0 ; i < n ; i++)


x[i] = x[i] + s;

Una posible implementación del este fragmento de código podría ser la siguiente:

lw r1, n ; r1 = número de iteraciones


ld f0, s ; f0 = s
add r3, r0, r0 ; r3 = deslazamiento del elemento en el vector

bucle: ld f2, x(r3) ; f2 = x[i]


add f4, f2, f0 ; f4 = x[i] + s
sd f4, x(r3) ; x[i] = f4
subi r1, r1, #1 ; Queda un elemento menos
addi r3, r3, #8 ; Desplazamiento para el siguiente elemento
bnez r1, bucle ; Saltamos si quedan elementos

La Tabla 1 muestra una posible planificación del este código en un procesador VLIW. Como
se puede comprobar, debido a las latencias de las unidades de ejecución y a las dependencias
entre las instrucciones del cuerpo del bucle, aunque se puedan emitir hasta tres instrucciones
por ciclo, no se puede aprovechar todo este paralelismo. El código tardaría en ejecutarse
aproximadamente 6n + 2 ciclos para un bucle de n iteraciones y de las instrucciones
ejecutadas, sólo 6n + 3 de las operaciones emitidas serían útiles, mientras que sería necesario
introducir 12n + 3 operaciones nop (los huecos de la tabla) para poder construir las
instrucciones VLIW, es decir, que se estaría desaprovechando aproximadamente un 66% del
paralelismo disponible en el procesador.

ETIQ. OP ALU OP FP OP MEM


add r3, r0, r0 ld f0, s
lw r1, n
bucle ld f2, x(r3)

add f4, f2, f0


subi r1, r1, #1
bnez r1, bucle
addi r3, r3, #8 sd f4, x(r3)

Tabla 1. Planificación del código en un procesador VLIW.

El desenrollado de bucles consiste en repetir varias veces el código de una iteración del bucle
original en un nuevo bucle que itere menos veces. De esta forma, el bloque básico del cuerpo
del nuevo bucle estará compuesto por las instrucciones de varias iteraciones del bucle
original, lo que aumentará el número de instrucciones independientes. Por ejemplo, si
desenrollamos cinco veces el código anterior tendríamos el siguiente código:

lw r1, n ; r1 = número de iteraciones


ld f0, s ; f0 = s
add r3, r0, r0 ; r3 = deslazamiento del elemento en el vector

bucle: ld f2, x(r3) ; f2 = x[i]


add f4, f2, f0 ; f4 = x[i] + s
sd f4, x(r3) ; x[i] = f4

ld f6, x+8(r3) ; f6 = x[i + 1]


add f8, f6, f0 ; f8 = x[i + 1] + s
sd f8, x+8(r3) ; x[i + 1] = f8

ld f10, x+16(r3) ; f10 = x[i + 2]


add f12, f10, f0 ; f12 = x[i + 2] + s
sd f12, x+16(r3) ; x[i + 2] = f12

ld f14, x+24(r3) ; f14 = x[i + 3]


add f16, f14, f0 ; f16 = x[i + 3] + s
sd f16, x+24(r3) ; x[i + 3] = f16

ld f18, x+32(r3) ; f18 = x[i + 4]


add f20, f18, f0 ; f20 = x[i + 4] + s
sd f20, x+32(r3) ; x[i + 4] = f20

subi r1, r1, #5 ; Quedan cuatro elementos menos


addi r3, r3, #40 ; Desplazamiento para el siguiente elemento
bnez r1, bucle ; Saltamos si quedan elementos
La Tabla 2 muestra una posible planificación para el código desenrollado. Es fácil comprobar
que ahora el tiempo de ejecución es aproximadamente 2n + 2 ciclos, ya que aunque cada
iteración necesita 10 ciclos, se itera cinco veces menos que antes (se ha reducido un 33%) y
que se desaprovecha sólo un 40% del paralelismo (frente al 66% de antes). El efecto negativo
del desenrollado es el incremento en el tamaño del código. Como se puede ver comparando
la Tabla 1 con la Tabla 2, mientras que el código VLIW para el caso no desenrollado necesita
ocho instrucciones VLIW, en el caso desenrollado necesita doce instrucciones VLIW (un
incremento del 33% en el tamaño del código). En la mayoría de los casos habrá que llegar a
un compromiso entre la reducción en el tiempo de ejecución y el incremento de memoria
que se necesita para almacenar el código desenrollado.

ETIQ. OP ALU OP FP OP MEM


add r3, r0, r0 ld f0, s
lw r1, n
bucle ld f2, x(r3)
ld f6, x+8(r3)
add f4, f2, f0 ld f10, x+16(r3)
add f8, f6, f0 ld f14, x+24(r3)
add f12, f10, f0 ld f18, x+32(r3)
add f16, f14, f0 sd f4, x(r3)
add f20, f18, f0 sd f8, x+8(r3)
subi r1, r1, #5 sd f12, x+16(r3)
bnez r1, bucle sd f16, x+24(r3)
addi r3, r3, #40 sd f20, x+32(r3)

Tabla 2. Planificación del código desenrollado en un procesador VLIW.

Iteración i Iteración i + 1 Iteración i + 2

ld f2, x(r3)
add f4, f2, f0 ld f2, x+8(r3)
sd f4, x(r3) add f4, f2, f0 ld f2, x+16(r3)
sd f4, x+8(r3) add f4, f2, f0
sd f4, x+16(r3)
Figura 2. Construcción del cuerpo del bucle aplicando de segmentación software.

Una alternativa para reducir las dependencias entre instrucciones dentro del cuerpo de un
bucle sin tener que recurrir al incremento de tamaño que implica el desenrollado de bucles es
la segmentación software. Esta técnica se basa en que las instrucciones de carga de datos, de
operación con los datos cargados, y de almacenamiento de los resultados obtenidos del bucle
original se distribuyen en iteraciones diferentes del nuevo bucle, en lugar de encontrarse en la
misma iteración. Con esto se consigue construir un cuerpo del bucle en el que todas las
instrucciones de manejo de datos son independientes. La Figura 2 muestra cómo se
construye el cuerpo del bucle aplicando esta técnica al programa que estamos usando como
ejemplo. Como muestra la figura, no todas las instrucciones de las tres iteraciones del bucle
original pasan a formar parte del nuevo bucle, por lo que será necesario insertarlas como
prólogo y epílogo del nuevo bucle, como se muestra a continuación:

lw r1, n ; r1 = número de iteraciones


ld f0, s ; f0 = s
add r3, r0, r0 ; r3 = deslazamiento del elemento en el vector
prologo: ld f2, x(r3) ; f2 = x[i]
add f4, f2, f0 ; f4 = x[i] + s
ld f2, x+8(r3) ; f2 = x[i + 1]

bucle: sd f4, x(r3) ; x[i] = f4


add f4, f2, f0 ; f4 = x[i + 1] + s
ld f2, x+16(r3) ; f2 = x[i + 2]
subi r1, r1, #1 ; Queda un elemento menos
addi r3, r3, #8 ; Desplazamiento para el siguiente elemento
bnez r1, bucle ; Saltamos si quedan elementos

epilogo: sd f4, x+8(r3) ; x[i + 1] = f4


add f4, f2, f0 ; f4 = x[i + 2] + s
sd f4, x+16(r3) ; x[i + 2] = f4

La Tabla 3 muestra cómo se planificaría este código en el procesador VLIW que venimos
usando para este ejemplo. En este caso, el tiempo de ejecución es de unos 3n + 11 ciclos (se
ha reducido un 50% para valores de n grandes), y aún se podría reducir más si una vez
aplicada esta técnica, se desenrollara el bucle un par de veces, de forma que se pudieran
rellenar los tres slots vacíos que quedan en el cuerpo del bucle.

ETIQ. OP ALU OP FP OP MEM


add r3, r0, r0 ld f0, s
lw r1, n
prologo ld f2, x(r3)

add f4, f2, f0


ld f2, x+8(r3)

bucle subi r1, r1, #1 add f4, f2, f0 sd f4, x(r3)


bnez r1, bucle ld f2, x+16(r3)
addi r3, r3, #8
epilogo add f4, f2, f0 sd f4, x+8(r3)

sd f4, x+16(r3)

Tabla 3. Planificación del código con segmentación software en un procesador VLIW.

Mediante las técnicas anteriores se puede ampliar el tamaño de los bloques básicos
introducidos por los saltos de los bucles, de forma que se mejore la eficiencia del código
generado para un procesador VLIW. Sin embargo, en los programa también pueden aparecer
instrucciones de salto condicional para implementar otro tipo de sentencias, como por
ejemplo las sentencias if o switch. Para planificar eficientemente este tipo de sentencias es
necesario aplicar otra técnica denominada ejecución vigilada o predicación de instrucciones. Esta
técnica cambia las dependencias de control por dependencias RAW, consiguiendo ampliar el
tamaño de los bloques básicos.

Un predicando es un operando que determina si el resultado de la instrucción en la que se


utiliza se considera o no (o, lo que es lo mismo, si la operación se ejecuta o no). En algunos
repertorios de instrucciones, todas las instrucciones pueden predicarse, es decir, a toda
instrucción se le puede asignar un predicado que permite controlar si la instrucción se ejecuta
o no. En algunos repertorios, en cambio, sólo se pueden asignar predicados a determinadas
instrucciones. Así, por ejemplo, un formato usual para indicar que el predicado p se asigna a
la instrucción instr, es (p) instr, y significa que la instrucción sólo se ejecuta si p = 1. El valor
de un predicado se fija mediante instrucciones que evalúan si una condición es cierta o no y
según sea o no, asignan un valor distinto al predicado. Un formato usual puede ser
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre. Para mostrar el uso
de los predicados usaremos el siguiente ejemplo:

if (a && b)
j = j + 1;
else if (c)
k = k + 1;
else
k = k – 1;
i = i + 1;

Inicio

Si No
¿a ≠ 0?

p1
Si No
¿b ≠ 0?

p2
Si No
¿c ≠ 0?
p1 p4
p3
jj+1 kk–1
kk+1

ii+1

Fin

Figura 3. Organigrama que muestra el uso de predicados.

La Figura 3 muestra cómo se pueden aplicar predicados para generar el siguiente código sin
saltos condicionales, formando un único bloque básico, que se puede planificar como
muestra la Tabla 4 en un procesador VLIW con dos slots de emisión en el que las cargas de
memoria tienen una latencia de dos ciclos. Las dependencias de control pasan a ser
dependencias de datos y deben respetarse a la hora de ubicar las operaciones en las
instrucciones VLIW. Hay que tener en cuenta que, en una instrucción con predicado, el
predicado es un registro de entrada (se lee) y que en una instrucción de comparación que
determina valores de predicados, los predicados son registros de salida (se escribe sobre
ellos):
lw r1, a ; r1 = a
p1, p2 cmp.ne r1, r0 ; p1 = 1 si a != 0 (p2 = 1 en caso contrario)
(p1) lw r2, b ; r2 = b (si a != 0)
(p1) p1, p2 cmp.ne r2, r0 ; p1 = 1 si a != 0 y b != 0 (p2 = 1 en caso contrario)
(p1) lw r4, j ; r4 = j (si a != 0 y b != 0)
(p1) addi r4, r4, #1 ; r4 = j + 1 (si a != 0 y b != 0)
(p1) sw r4, j ; j = j + 1 (si a != 0 y b != 0)
(p2) lw r3, c ; r3 = c (si a == 0 ó b == 0)
(p2) lw r5, k ; r5 = k (si a == 0 ó b == 0)
(p2) p3 cmp.ne r0, r0 ; p3 = 0 (si a == 0 ó b == 0)
(p2) p4 cmp.ne r0, r0 ; p4 = 0 (si a == 0 ó b == 0)
(p2) p3, p4 cmp.ne r3, r0 ; p3 = 1 ((si a == 0 ó b == 0) y c !=0)
(p3) addi r5, r5, #1 ; r5 = k + 1 ((si a == 0 ó b == 0) y c !=0)
(p4) subi r5, r5, #1 ; r5 = k – 1 ((si a == 0 ó b == 0) y c ==0)
(p2) sw r5, k ; k = r5 (si a == 0 ó b == 0)
lw r6, i ; r6 = i
addi r6, r6, #1 ; r6 = i + 1
sw r6, i ;i=i+1

#. OP1 OP2
1 lw r1, a
2 lw r6, i
3 p1, p2 cmp.ne r1, r0
4 addi r6, r6, #1 (p1) lw r2, b
5 sw r6, i
6 (p1) p1, p2 cmp.ne r2, r0
7 (p2) p3 cmp.ne r0, r0 (p1) lw r4, j
8 (p2) p4 cmp.ne r0, r0 (p2) lw r3, c
9 (p1) addi r4, r4, #1 (p2) lw r5, k
10 (p2) p3, p4 cmp.ne r3, r0 (p1) sw r4, j
11 (p3) addi r5, r5, #1 (p4) subi r5, r5, #1
12
13 (p2) sw r5, k

Tabla 4. Planificación del código con predicados en un procesador VLIW.

Las instrucciones con predicado pueden utilizarse para adelantar especulativamente


operaciones y pasarlas de un bloque básico a otro. De esta manera se puede facilitar el
trabajo del compilador a la hora de generar programas VLIW más rápidos y/o con un uso
más eficiente de la memoria. Esto se ilustra con el código de la Tabla 5, en el que hay una
instrucción de salto condicional (beqz) que, en el caso de que el registro r10 sea igual a cero,
daría lugar a un salto a una dirección de memoria (loop) posterior a la de las dos instrucciones
que la siguen para evitar que la instrucción 4 intente cargar un dato de la dirección 0x0000.

#. OP1 OP2
1 lw r1, a
2 add r3, r4, r5
3 beqz r10, loop add r6, r3, r7
4 lw r8, 0(r10)
5
6 lw r9, 0(r8)

Tabla 5. Ejemplo de código VLIW.


En la Tabla 6 se muestra cómo se adelanta especulativamente la carga de la instrucción 4
para ocultar su latencia. Aunque semánticamente ambos códigos son equivalentes, este
último permite reducir el número de instrucciones VLIW y hacer un uso más eficiente de la
memoria.

#. OP1 OP2
1 lw r1, a p1 cmp.ne r10, r0
2 (p1) lw r8, 0(r10) add r3, r4, r5
3 beqz r10, loop add r6, r3, r7
4 lw r9, 0(r8)

Tabla 6. Uso de predicados para implementar una carga especulativa en el código de la Tabla 5.

Sin embargo, el comportamiento de ambos códigos puede no ser equivalente frente a


posibles excepciones. Por ejemplo, en el código de la Tabla 5, si r10 es distinto de cero no se
producen los accesos a memoria de las instrucciones de carga que siguen a la instrucción de
salto condicional. Así, si r10 es cero, es decir, una dirección no permitida para el acceso a
memoria, no se generarían las cargas y no habría excepciones. En el caso de la Tabla 6, lo
que ocurra depende de la forma en que se hayan implementado las instrucciones con
predicado. Si se produce el acceso a memoria y posteriormente se carga o no r8 con el
resultado de ese acceso después de evaluar r10, se producirá la excepción de todas formas
dado que el acceso a memoria se ha generado. Existen distintas alternativas para asegurar un
comportamiento coherente frente a las excepciones cuando hay movimientos de código
especulativos [ORT05]. Entre ellas están los bits de veneno, el uso de centinelas, etc. Algunas
de esas técnicas se basan en el uso de estructuras similares a los ROB. Por lo tanto, se pone
de manifiesto que el aprovechamiento eficiente de las arquitecturas VLIW en procesadores
de propósito general precisa de la inclusión de ciertas estructuras hardware que faciliten el
trabajo del compilador, de la misma forma que en los procesadores superescalares.
Problemas
1. Suponga un procesador VLIW cuyas instrucciones pueden codificar hasta cuatro
operaciones con las restricciones que se muestran en la Tabla 7. En dicha tabla también se
incluye información de las latencias de cada una de las unidades funcionales disponibles que
se utilizan en este ejercicio. Muestre una forma posible de ejecutar el siguiente código:

void sum(int c[ ], int a[ ], int b[ ], int n)


{
int i;
for (i = 0 ; i < n ; i++)
c[i] = a[i] + b[i];
}

NOTA: Se sugiere desenrollar el bucle (hasta cuatro iteraciones) para optimizar el velocidad de
ejecución del programa.

UNIDAD
LATENCIA OP1 OP2 OP3 OP4 OPERACIONES REALIZADAS
FUNCIONAL
Comparaciones, sumas y restas
ALU 1 X X X X
con enteros y operaciones lógicas

Memoria 3 X X Cargas y almacenamientos

Saltos condicionales e
Saltos 4 X
incondicionales

Tabla 7. Especificaciones del procesador VLIW del problema 1.

Solución
El programa en ensamblador sin desenrollar el bucle puede ser el siguiente:

lw r1, n ; Número de elementos que quedan por procesar


add r2, r0, r0 ; Desplazamiento de los datos en los vectores
inicio: lw r3, a(r2) ; Cargamos a[i]
lw r4,b(r2) ; Cargamos b[i]
add r5, r3, r4 ; c[i]=a[i]+b[i]
sw r5, c(r2) ; Almacenamos c[i]
addi r2, r2, #4 ; Avanzamos hasta el siguiente dato
subi r1 ,r1, #1 ; Queda un elemento menos
bnez r1, ,inicio ; Saltamos si quedan elementos por procesar

Si se desenrolla el bucle cuatro veces, y suponiendo que n es múltiplo de cuatro, el código quedaría:

lw r1, n ; Número de elementos que quedan por procesar


add r2, r0, r0 ; Desplazamiento de los datos en los vectores

inicio: lw r3, a(r2) ; Cargamos a[i]


lw r4,b(r2) ; Cargamos b[i]
add r5, r3, r4 ; c[i]=a[i]+b[i]
sw r5, c(r2) ; Almacenamos c[i]

lw r6, a+4(r2) ; Cargamos a[i + 1]


lw r7,b+4(r2) ; Cargamos b[i + 1]
add r8, r6, r7 ; c[i + 1]=a[i + 1]+b[i + 1]
sw r8, c+4(r2) ; Almacenamos c[i + 1]
lw r9, a+8(r2) ; Cargamos a[i + 2]
lw r10,b+8(r2) ; Cargamos b[i + 2]
add r11, r9, r10 ; c[i + 2]=a[i + 2]+b[i + 2]
sw r11, c+8(r2) ; Almacenamos c[i + 2]

lw r12, a+12(r2) ; Cargamos a[i + 3]


lw r13,b+12(r2) ; Cargamos b[i + 3]
add r14, r12, r13 ; c[i + 3]=a[i + 3]+b[i + 3]
sw r14, c+12(r2) ; Almacenamos c[i + 3]

addi r2, r2, #16 ; Avanzamos hasta el siguiente grupo de 4 datos


subi r1 ,r1, #4 ; Quedan cuatro elementos menos
bnez r1, ,inicio ; Saltamos si quedan elementos por procesar

Reorganizando estas instrucciones y asignándolas a las operaciones posibles dentro del procesador
VLIW, se definen las instrucciones del mismo como indica la Tabla 8. Es esta tabla se puede
comprobar cómo se debe adelantar el salto para aprovechar los tres ciclos siguientes mientras se
resuelve. Como se puede ver, en este caso no se pueden ocupar 15 de los 36 huecos existentes para las
operaciones de estas 9 instrucciones VLIW. Es decir, se desperdicia un 41.7% del espacio. Se ha
supuesto que las dependencias WAR y WAW las resuelve el hardware de forma que se pueden incluir
en una misma instrucción VLIW operaciones con el mismo operando.

ETIQUETA OP1 OP2 OP3 OP4

lw r1, n add r2, r0, r0


inicio: lw r3, a(r2) lw r4,b(r2)
lw r6, a+4(r2) lw r7,b+4(r2)
lw r9, a+8(r2) lw r10,b+8(r2) subi r1 ,r1, #4
lw r12, a+12(r2) lw r13,b+12(r2) add r5, r3, r4
sw r5, c(r2) bnez r1, ,inicio add r8, r6, r7
sw r8, c+4(r2) add r11, r9, r10
sw r11, c+8(r2) add r14, r12, r13
sw r14, c+12(r2) addi r2, r2, #16

Tabla 8. Código VLIW para el procesador del problema 1.

2. El siguiente fragmento de código implementa el producto escalar de dos vectores:

lw r1, n ; Número de elementos que quedan por procesar


add r2, r0, r0 ; Desplazamiento de los datos en los vectores
subd f0, f0, f0 ; Inicializamos el acumulador a cero

inicio: ld f2, x(r2) ; Cargamos X[i]


ld f4, y(r2) ; Cargamos Y[i]
muld f6, f2, f4 ; X[i]*Y[i]
addd f0, f0, f6 ; sum=sum+X[i]*Y[i]
addi r2, r2, #8 ; Desplazamiento para los siguiente elementos
subi r1, r1, #1 ; Queda un elemento menos
bnez r1, inicio ; Saltamos si quedan elementos
sd f0, z ; Almacenamos el resultado

a) Desenrolle el bucle y optimice el código para minimizar los retardos


introducidos por las dependencias entre las instrucciones en un cauce
sencillo en el que sólo se emite una instrucción por ciclo, suponiendo las
latencias que se muestran en la Tabla 9.

b) Muestre cual sería la forma de ejecutar el código del apartado anterior en un


procesador VLIW con las mismas latencias en las distintas operaciones y
que puede emitir dos operaciones por ciclo, de forma que las operaciones
de acceso a memoria y los saltos sólo se pueden emitir por el primer slot de
emisión y las operaciones de coma flotante por el segundo.

c) ¿Cuál sería la máxima ganancia en velocidad que se podría llegar a obtener al


usar el procesador VLIW?

INSTRUCCIÓN QUE LATENCIA


GENERA EL RESULTADO (CICLOS)
Operación en coma flotante 3
Carga 2
Almacenamientos 1
Saltos 1
ALU 1

Tabla 9. Latencias de las unidades de ejecución del procesador del problema 2.

Solución
En el código que se proporciona en el enunciado existen bastantes latencias entre las instrucciones que
lo componen (por ejemplo, entre la multiplicación y la suma en coma flotante que aparecen). Se
pueden reducir los atascos causados por esas latencias desenrollado el bucle y reorganizando las
instrucciones, tras renombrar algunos registros. Así, si se desenrolla el bucle dos veces, y suponiendo
que n es par, se obtiene:

lw r1, n ; (1) Número de elementos que quedan por procesar


add r2, r0, r0 ; (2) Desplazamiento de los datos en los vectores
subd f0, f0, f0 ; (3) Inicializamos el acumulador a cero

inicio: ld f2, x(r2) ; (4) Cargamos X[i]


ld f4, y(r2) ; (5) Cargamos Y[i]
ld f8, x+8(r2) ; (6) Cargamos X[i+1]
muld f6, f2, f4 ; (7) X[i]*Y[i]
ld f10, y+8(r2) ; (8) Cargamos Y[i+1]
addi r2, r2, #16 ; (9) Desplazamiento para los siguiente elementos
muld f12, f8, f10 ; (10) X[i+1]*Y[i+1]
addd f0, f0, f6 ; (11) sum = sum + X[i]*Y[i]
subi r1, r1, #2 ; (12) Queda un elemento menos
nop ; (13)
bnez r1, inicio ; (15) Saltamos si quedan elementos
addd f0, f0, f12 ; (14) sum = sum + X[i+1]*Y[i+1]

nop ; (16)
nop ; (17)
sd f0, z ; (18) Almacenamos el resultado

Teniendo en cuenta las latencias entre las distintas instrucciones, se observa que se perderían n + 2
ciclos (mostrados explícitamente mediante instrucciones nop). Los n primeros se perderían de uno en
uno, en cada una de las iteraciones del bucle, entre las instrucciones (10) y (14) dado que las
operaciones en coma flotante tienen una latencia igual a 4 ciclos si el resultado va a ser utilizado por
otra operación de coma flotante. Los dos ciclos perdidos al final se deben a la dependencia entre las
instrucciones (14) y (18), ya que tras una operación en coma flotante se deben esperar tres ciclos para
almacenar el resultado. Para mejorar las prestaciones se ha supuesto que el procesador utiliza un salto
retardado en el que se puede aprovechar la siguiente instrucción al salto. El tiempo estimado de
ejecución de este programa sería de unos:
n
Tseg n   3  12  3  6n  6
2

En el caso del procesador VLIW que puede emitir hasta dos operaciones por ciclo, se tendrá el código
mostrado en la Tabla 10. Debido a las altas latencias de las unidades de procesamiento de números en
coma flotante, el tiempo de ejecución no mejora demasiado. Como se ha podido solapar la ejecución
de tres instrucciones con respecto al código anterior, el tiempo de ejecución estimado sería de:

n
TVLIWn   2  10  3  5n  5
2

ETIQUETA OP1 OP2

lw r1, n subd f0, f0, f0


add r2, r0, r0
inicio: ld f2, x(r2)
ld f4, y(r2)
ld f8, x+8(r2)
ld f10, y+8(r2) muld f6, f2, f4
addi r2, r2, #16
subi r1, r1, #2 muld f12, f8, f10
addd f0, f0, f6

bnez r1, inicio


addd f0, f0, f12

sd f0, z

Tabla 10. Código VLIW para el procesador del problema 2.

Por tanto, la ganancia en velocidad obtenida sería de:

Tseg n  6n  6
S n   
TVLIWn  5n  5

y la máxima ganancia que se podría llegar a obtener:

6n  6
S max  lim S n   lim  1.2
n  n  5n 5

3. Dada la secuencia de instrucciones VLIW mostrada en la Tabla 11,

a) Indique cómo puede mejorarse el comportamiento de este código utilizando


la forma condicional de la instrucción lw.

b) Indique cómo transformaría la secuencia de instrucciones de forma que sólo


se utilicen instrucciones de movimiento de datos condicionales (no debe
haber ninguna instrucción de salto condicional). Asegúrese de que se
mantenga el comportamiento del procesador frente a las posibles
excepciones.

NOTA: El procesador puede emitir, cada ciclo, una combinación de operación de referencia a
memoria y operación a ALU, o una única operación de salto condicional.
# OP1 OP2

1 lw r1, x(r2) add r10, r11, r12


2 add r13, r10, r14
3 beqz r3, direc
4 lw r4, 0(r3)
5 lw r5, 0(r4)

Tabla 11. Código VLIW del problema 3.

Solución
La distribución de operaciones entre las distintas instrucciones VLIW presenta un hueco en el slot 1 de
la segunda instrucción. Además, si no se produce el salto existe una dependencia RAW entre las
instrucciones (4) y (5) que producirá atascos. Con una instrucción de carga condicional lwc que realice
la carga desde memoria en función del valor de r3 se podría mejorar la situación. Concretamente, se
podría utilizar la instrucción lwc r4, 0(r3), r3, que cargaría el valor de r4 cuando r3 sea distinto de cero,
tal y como indica la Tabla 12, lo que reduciría el código en una instrucción.

# OP1 OP2

1 lw r1, x(r2) add r10, r11, r12


2 lwc r4, 0(r3), r3 add r13, r10, r14
3 beqz r3, direc
4 lw r5, 0(r4)

Tabla 12. Optimización del código VLIW del problema 3 mediante instrucciones de carga condicional.

Para responder al segundo apartado, vamos a considerar sólo las instrucciones que se incluyen en el
campo de operación 1, ya que las del segundo slot son independientes y no afectarán a los cambios que
vamos a realizar en el código. Por lo tanto, nos centraremos en eliminar los saltos condicionales de la
siguiente secuencia de instrucciones:

lw r1, x(r2) ; (1)


nop ; (2)
beqz r3, direc ; (3)
lw r4, 0(r3) ; (4)
lw r5, 0(r4) ; (5)

En esta secuencia de instrucciones, la instrucción de salto (3) se introduce para que no se ejecute la
instrucción de carga (4) si r3 = 0. Por tanto, se puede suponer que la instrucción de salto tiene la
función de evitar una violación del acceso a memoria. Así, si se adelantara la instrucción (4) para que
estuviera delante de la instrucción de salto, la excepción que se produciría si r3 fuera igual a 0 haría que
el programa terminase. Para que este cambio pueda realizarse es necesario que r3 sea distinto de cero
siempre. En este caso, si existen registros disponibles, es posible utilizar instrucciones de movimiento
condicional para evitar que se produzca la carga en caso de que se vaya a producir la excepción. El
código sería:

addi r6, r0, #1000 ; Fijamos r6 a una dirección segura


lw r1, x(r2)
mov r7, r4 ; Guardamos el contenido original de r4 en r7
cmovnz r6, r3, r3 ; Movemos r3 a r6 si r3 es distinto de cero
lw r4, 0(r6) ; Carga especulativa
cmovz r4, r7, r3 ; Si r3 es 0, hay que hacer que r4 recupere su valor
beqz r3, direc
lw r5, 0(r4) ; Si r3 no es cero, hay que cargar r5

donde r6 y r7 son registros auxiliares. En r6 se carga primero una dirección segura, y en r7 se introduce
el valor previo de r4 para poder recuperarlo si la carga especulativa no debía realizarse. Como se puede
comprobar, la especulación tiene un coste en instrucciones cuyo efecto final en el tiempo de ejecución
depende de la probabilidad de que la especulación sea correcta o no.

Es posible evitar la instrucción de salto si se utilizan cargas especulativas para las dos instrucciones de
carga protegidas por el salto en el código inicial. En este caso el código sería el siguiente:

addi r6, r0, #1000 ; Fijamos r6 a una dirección segura


lw r1, x(r2)
mov r7, r4 ; Guardamos el contenido original de r4 en r7
mov r8, r5 ; Guardamos r5 en otro registro temporal r8
cmovnz r6, r3, r3 ; Movemos r3 a r6 si r3 es distinto de cero
lw r4, 0(r6) ; Carga especulativa
lw r5, 0(r4) ; Esta carga también es especulativa
cmovz r4, r7, r3 ; Si r3 es 0, hay que hacer que r4 recupere su valor
cmovz r5, r8, r3 ; Si r3 es 0 hay que hacer que r5 recupere su valor

Se supone que la dirección direc a la que se produce el salto viene a continuación de este trozo de
código. Si no fuese así, no se podría aprovechar tan eficientemente el procesamiento especulativo. Así,
en general, cuando existen saltos a distintas direcciones y no existe un punto de confluencia de esos
caminos no suele ser posible obtener mejores prestaciones mediante cambios especulativos que
eliminen la instrucción de salto.

4. En un procesador todas las instrucciones pueden predicarse. Para establecer los valores de
los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

En estas condiciones, utilice instrucciones con predicado para escribir sin ninguna
instrucción de salto el siguiente código:

if (a > b)
x = 1;
else
{
if (c < d)
x = 2;
else
x = 3;
}

Solución
A continuación se muestra el código que implementa el programa anterior usando predicados:

lw r1, a ; r1 = A
lw r2, b ; r2 = B
p1, p2 cmp.gt r1,r2 ; Si a > b p1 = 1 y p2 = 0 (si no, p1 = 0 y p2 = 1)
(p1) addi r5, r0, #1
p3 cmp.ne r0, r0 ; Inicializamos p3 a 0
p4 cmp.ne r0, r0 ; Inicializamos p4 a 0
(p2) lw r3, c ; r3 = c
(p2) lw r4, d ; r4 = d
(p2) p3, p4 cmp.lt r3, r4 ; Sólo si p2 = 1 p3 o p4 pueden ser 1
(p3) addi r5, r0, #2 ; Se ejecuta si p3 = 1 (y p2 = 1)
(p4) addi r5, r0, #3 ; Se ejecuta si p4 = 1 (y p2 = 1)
sw r5, x ; Almacenamos el resultado

5. Suponga un procesador en el que todas las instrucciones pueden predicarse. Para establecer
los valores de los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

a) En estas condiciones, escriba la secuencia de instrucciones máquina que


implementarían el siguiente código sin utilizar ninguna instrucción de salto:

x = 0;
if ((a  b) && (b  0))
x = 1;
else if ((b < 0) && (a < b))
x = 2;

b) Optimice el código anterior para un procesador VLIW con dos slots de


emisión, en el que las instrucciones de comparación sólo pueden colocarse
en el primero de ellos y las latencias de las operaciones son de 1 ciclo para
las sumas, restas y comparaciones, de dos ciclos para las multiplicaciones y
de tres ciclos para las cargas de memoria.

Solución
La Figura 4 muestra el organigrama que implementa la secuencia de código del enunciado, en el que se
han resaltado los predicados que se usarán para sustituir cada uno de los saltos. A partir de esta figura
es sencillo escribir el siguiente código:

Inicio

x←0

Si No
¿a ≥ b?
p1
Si No
¿b ≥ 0?
p2
p1
Si No
x←1 ¿b < 0?
p2
Si No
¿a < b?
p2
x←2

Fin

Figura 4. Organigrama del problema 5.


lw r1, a ; r1 = a
lw r2, b ; r2 = b
add r3, r0, r0 ; r3 = 0

p1, p2 cmp.ge r1, r2 ; ¿a >= b?


(p1) p1, p2 cmp.ge r2, r0 ; ¿b >= 0?
(p2) p2 cmp.lt r2, r0 ; ¿b < 0?
(p2) p2 cmp.lt r1, r2 ; ¿a < b?

(p1) addi r3, r0, #1 ; r3 = 1


(p2) addi r3, r0, #2 ; r3 = 2
sw r3, x ; x = r3

Una vez escrito el código, sólo nos queda optimizarlo para la arquitectura VLIW propuesta en el
enunciado, tal y como se muestra en la Tabla 13.

# SLOT 1 SLOT 2
1 lw r1, a lw r2, b
2 add r3, r0, r0
3
4 p1, p2 cmp.ge r1, r2
5 (p1) p1, p2 cmp.ge r2, r0
6 (p2) p2 cmp.lt r2, r0
7 (p2) p2 cmp.lt r1, r2
8 (p1) addi r3, r0, #1 (p2) addi r3, r0, #2
9 sw r3, x

Tabla 13. Optimización del código VLIW del problema 5.

6. En un procesador, todas las instrucciones pueden predicarse. Para establecer los valores de
los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

a) En estas condiciones, escriba sin ninguna instrucción de salto, el código


para la siguiente secuencia de instrucciones:

if ((a > b) && (a > c))


x = 2 * x;
else if ((a < b) && (a < c))
x = 4 * x;

b) Indique entre qué instrucciones existen riesgos de tipo RAW.

c) ¿Cómo situaría las instrucciones escalares obtenidas para obtener el mínimo


tiempo de ejecución en un procesador VLIW cuyas instrucciones largas
tienen espacio para tres instrucciones escalares (slots), suponiendo que
cualquier instrucción puede ir a cualquier slot?

NOTA: Suponga que la ejecución de todas las instrucciones puede hacerse en un ciclo, y si hay
dependencia de tipo RAW entre instrucciones consecutivas hay que retrasar un ciclo la
segunda. En el caso de las dependencias WAR y WAW no se introducen retardos.
Solución
La Figura 5 muestra un posible organigrama para el código del enunciado.

Inicio

Si No
¿a > b?
p1
Si No
¿a > c?
p2
Si No
¿a < b?
p4
p3 Si No
¿a < c?
x2*x p5

x4*x

Fin

Figura 5. Organigrama del problema 6.

Del organigrama se deriva el siguiente código ensamblador:

lw r2, a ; (1) Cargamos a


lw r3, b ; (2) Cargamos b
lw r4, c ; (3) Cargamos c
lw r5, x ; (4) Cargamos x

p3 cmp.ne r0, r0 ; (5) Inicializamos p3 a 0


p4 cmp.ne r0, r0 ; (6) Inicializamos p4 a 0
p5 cmp.ne r0, r0 ; (7) Inicializamos p5 a 0

p1, p2 cmp.gt r2, r3 ; (8) ¿a > b?


(p1) p3, p2 cmp.gt r2, r4 ; (9) ¿a > c?
(p2) p4 cmp.lt r2, r3 ; (10) ¿a < b?
(p4) p5 cmp.lt r2, r4 ; (11) ¿a < c?

(p3) slli r5, r5, #1 ; (12) x = x * 2


(p5) slli r5, r5, #2 ; (13) x = x * 4

sw r5, x ; (14) Almacenamos x

Las dependencias RAW del código son las siguientes:

 Las instrucciones (8) y (10) depende de la (1) por r2 y de la (2) por r3

 Las instrucciones (9) y (11) depende de la (1) por r2 y de la (3) por r4

 La instrucción (9) depende de la instrucción (8) por p1

 La instrucción (10) depende de las instrucciones (8) y (9) por p2

 La instrucción (11) depende de la instrucción (10) por p4


 La instrucción (12) depende de la instrucción (9) por p3

 La instrucción (13) depende de la instrucción (11) por p5

 Las instrucciones (12) y (13) dependen de la instrucción (4) por r5

 La instrucción (14) depende de las instrucciones (12) y (13) por r5

Teniendo en cuenta estas dependencias RAW, y que hay que incluir un ciclo entre cada dos
instrucciones con riesgos RAW, podríamos formar las instrucciones VLIW que muestra la Tabla 14.
El gran número de slots vacíos se debe a la cadena de dependencias existente entre las instrucciones
(1), (2), (8), (9), (10), (11), (13) y (14).

# SLOT 1 SLOT 2 SLOT 3


1 lw r2, a lw r3, b
2 p3 cmp.ne r0, r0 p4 cmp.ne r0, r0 p5 cmp.ne r0, r0
3 p1, p2 cmp.gt r2, r3 lw r4, c
4
5 (p1) p3, p2 cmp.gt r2, r4 lw r5, x
6
7 (p2) p4 cmp.lt r2, r3 (p3) slli r5, r5, #1
8
9 (p4) p5 cmp.lt r2, r4
10
11 (p5) slli r5, r5, #2
12
13 sw r5, x

Tabla 14. Optimización del código VLIW del problema 6.

7. En un procesador, todas las instrucciones pueden predicarse. Para establecer los valores de
los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

a) En estas condiciones, escriba sin ninguna instrucción de salto, el código


para:

if ((a > b) || (a > c))


x = 2 * x;
else if ((a < b) && (a < c))
x = x / 2;

b) ¿Cómo situaría las instrucciones escalares obtenidas para obtener el mínimo


tiempo de ejecución en un procesador VLIW cuyas instrucciones largas
tienen espacio para tres instrucciones escalares (slots), suponiendo que
cualquier instrucción puede ir a cualquier slot?

NOTA: Suponga que la ejecución de todas las instrucciones puede hacerse en un ciclo.
Solución
El realizar un organigrama que refleje el comportamiento del código ayuda bastante a la resolución de
este tipo de problemas, sobre todo a la hora de decidir qué predicados se deben utilizar y dónde hay
que colocarlos. Un posible organigrama para este problema podría ser el que indica la Figura 6.

Inicio

Si No
¿a > b?

p2
Si No
¿a > c?

p1 p3
Si No
xx*2 ¿a < b?

p4
Si No
¿a > c?
p5

xx/2

Fin

Figura 6. Organigrama del problema 7.

Una vez diseñado el organigrama, hay que colocar un predicado en cada una de las instrucciones que
deban estar vigiladas, es decir que dependan de alguna condición, y tras esto, la traducción a código es
directa:

lw r2, a ; (1) Cargamos a


lw r3, b ; (2) Cargamos b
lw r4, c ; (3) Cargamos c
lw r5, x ; (4) Cargamos x

p1,p2 cmp.gt r2, r3 ; (5) ¿a > b?


p3 cmp.ne r0, r0 ; (6) Inicializo p3
p4 cmp.ne r0, r0 ; (7) Inicializo p4
p5 cmp.ne r0, r0 ; (8) Inicializo p5
(p2) p1,p3 cmp.gt r2, r4 ; (9) ¿a > c?
(p3) p4 cmp.lt r2, r3 ; (10) ¿a < b?
(p4) p5 cmp.lt r2, r4 ; (11) ¿a < c?

(p1) slli r5, r5, #1 ; (12) x = x * 2


(p5) srai r5, r5, #1 ; (13) x = x / 2

sw r5, x ; (14) Almacenamos x

Teniendo en cuenta esto código, y que cualquier instrucción puede alojarse en cualquiera de los tres
slots de emisión, una posible colocación de las instrucciones podría ser la que indica la Tabla 15.

# SLOT 1 SLOT 2 SLOT 3


1 lw r2, a lw r3, b p3 cmp.ne r0, r0
2 p1,p2 cmp.gt r2, r3 lw r4, c lw r5, x
3 (p2) p1,p3 cmp.gt r2, r4 p4 cmp.ne r0, r0 (p1) slli r5, r5, #1
4 (p3) p4 cmp.lt r2, r3 p5 cmp.ne r0, r0
5 (p4) p5 cmp.lt r2, r4
6 (p5) srai r5, r5, #1
7 sw r5, x

Tabla 15. Optimización del código VLIW del problema 7.

8. En un procesador VLIW cuyas instrucciones pueden codificar tres operaciones (tres campos
o slots en cada instrucción VLIW), todas las operaciones pueden predicarse. Para establecer
los valores de los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

Indique cómo sería el código VLIW para siguiente sentencia sin ninguna operación de salto,
teniendo en cuenta que las instrucciones de comparación sólo pueden aparecer en el primer
campo o slot de la instrucción VLIW (el resto de las instrucciones pueden aparecer en
cualquier campo). Considere que dispone del número de unidades funcionales necesarias en
cada momento.

if (x > 3)
{
y = x;
x = x / 2;
}
else if ((x > 0) && (x < 1))
{
y = – x;
x = 2 * x;
}

Solución
Como en el enunciado nos indican que la secuencia de instrucciones debe quedar sin ningún salto,
debemos predicar las instrucciones que contiene en su interior. La Figura 7 muestra el organigrama del
programa una vez que se han predicado las instrucciones. De este organigrama se puede derivar el
siguiente código ensamblador:

addi r1, r0, #1 ; r1 = 1


addi r3, r0, #3 ; r3 = 3

lw r5, x ; r5 = x
p1, p2 cmp.gt r5, r3 ; p1 = 1 si x > 3
p3 cmp.ne r0, r0 ; p3 = 0
p4 cmp.ne r0, r0 ; p4 = 0
(p1) sw r5, y ;y=x
(p1) sra r6, r5, r1 ; r6 = x / 2
(p1) sw r6, x ; x = r6
(p2) p3 cmp.gt r5, r0 ; p3 = 1 si x > 0
(p3) p4 cmp.lt r5, r1 ; p4 = 1 si x < 1
(p4) sub r7, r0, r5 ; r7 = –x
(p4) sw r7, y ; y = –x
(p4) sll r8, r5, r1 ; r8 = 2*x
(p4) sw r8, x ; x = r8
Inicio

Si No
¿x > 3?
p1
p2
yx Si No
¿x > 0?
xx/2
p3
No Si
¿x < 1?

p4

y–x
x  2*x

Fin

Figura 7. Organigrama del problema 8.

A partir de este código, sólo nos queda reorganizar el código respetando las dependencias de datos
para construir las instrucciones VLIW, tal y como muestra la Tabla 16.

# SLOT 1 SLOT 2 SLOT 3


1 p3 cmp.ne r0, r0 addi r1, r0, #1 addi r3, r0, #3
2 p4 cmp.ne r0, r0 lw r5, x
3 p1, p2 cmp.gt r5, r3
4 (p2) p3 cmp.gt r5, r0 (p1) sw r5, y (p1) sra r6, r5, r1
5 (p3) p4 cmp.lt r5, r1 (p1) sw r6, x
6 (p4) sub r7, r0, r5 (p4) sll r8, r5, r1
7 (p4) sw r7, y (p4) sw r8, x

Tabla 16. Optimización del código VLIW del problema 8.

9. En un procesador VLIW cuyas instrucciones pueden codificar tres operaciones (tres campos
o slots en cada instrucción VLIW), todas las operaciones pueden predicarse. Para establecer
los valores de los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

Indique cómo se escribiría la siguiente sentencia sin ninguna operación de salto y con el
mínimo número de instrucciones VLIW, teniendo en cuenta que las instrucciones de
comparación sólo pueden aparecer en el primer campo o slot de la instrucción VLIW (el resto
de las instrucciones pueden aparecer en cualquier campo). Considere que dispone del
número de unidades funcionales que necesite en cada momento.
for (i = 0 ; i < 2 ; i++)
{
if (x[i] > 2)
{
y[i] = x[i];
x[i] = 3 * x[i];
}
else if (x[i] > 0)
{
y[i] = – x[i];
x[i] = 5 * x[i];
}
}

Solución
Como en el enunciado nos indican que la secuencia de instrucciones debe quedar sin ningún salto,
debemos desenrollar el bucle y predicar las instrucciones que contiene en su interior. La Figura 8
muestra el organigrama del programa una vez que se ha desenrollado el bucle, destacando los
predicados que se usarán para evitar el uso de instrucciones de salto. A continuación se muestra el
código ensamblador derivado del organigrama:

Inicio

Si No
p1 ¿x[0] > 2?
p2
y[0]  x [0] Si No
p3 ¿x [0] > 0?
x[0]  3 * x [0]
y[0]  – x [0]
x [0]  5 * x [0]

Si No
p4 ¿x [1] > 2?
p5
y [1]  x [1] Si
¿x [1] > 0?
No
p6
x [1]  3 * x [1]
y[1]  – x [1]
x [1]  5* x [1]

Fin

Figura 8. Organigrama del problema 9.

addi r13, r0, #3 ; r13  3


addi r15, r0, #5 ; r15  5

lw r1, x ; r1  x[0]
subi r2, r1, #2 ; r2  x[0] – 2
p1, p2 cmp.gt r2, r0 ; p1  1 si x[0] > 2
p3 cmp.ne r0, r0 ; p3  0
(p1) sw r1, y ; y[0]  x[0]
(p1) mult r3, r1, r13 ; r3  3*x[0]
(p1) sw r3, x ; x[0]  3*x[0]
(p2) p3 cmp.gt r1, r0 ; p3  1 si x[0] > 0
(p3) sub r4, r0, r1 ; r4  –x[0]
(p3) sw r4, y ; y[0]  –x[0]
(p3) mult r5, r1, r15 ; r5  5*x[0]
(p3) sw r5, x ; x[0]  5*x[0]

lw r6, x+4 ; r6  x[1]


subi r7, r6, #2 ; r7  x[1] – 2
p4, p5 cmp.gt r7, r0 ; p4  1 si x[1] > 2
p6 cmp.ne r0, r0 ; p6  0
(p4) sw r6, y+4 ; y[1]  x[1]
(p4) mult r8, r6, r13 ; r8  3*x[1]
(p4) sw r8, x+4 ; x[1]  3*x[1]
(p5) p6 cmp.gt r6, r0 ; p6  1 si x[1] > 0
(p6) sub r9, r0, r6 ; r9  –x[1]
(p6) sw r9, y+4 ; y[1]  –x[1]
(p6) mult r10, r6, r15 ; r10  5*x[1]
(p6) sw r10, x+4 ; x[1]  5*x[1]

Tras desenrollar e introducir las operaciones con predicados, sólo nos queda reorganizar el código
respetando las dependencias de datos para construir las instrucciones VLIW. El resultado se muestra
en la Tabla 17.

# SLOT 1 SLOT 2 SLOT 3


1 p3 cmp.ne r0, r0 lw r1, x lw r6, x+4
2 p6 cmp.ne r0, r0 subi r2, r1, #2 subi r7, r6, #2
3 p1, p2 cmp.gt r2, r0 addi r13, r0, #3 addi r15, r0, #5
4 p4, p5 cmp.gt r7, r0 (p1) sw r1, y (p1) mult r3, r1, r13
5 (p2) p3 cmp.gt r1, r0 (p4) sw r6, y+4 (p4) mult r8, r6, r13
6 (p5) p6 cmp.gt r6, r0 (p1) sw r3, x (p4) sw r8, x+4
7 (p3) sub r4, r0, r1 (p6) sub r9, r0, r6 (p3) mult r5, r1, r15
8 (p3) sw r4, y (p6) sw r9, y+4 (p6) mult r10, r6, r15
9 (p3) sw r5, x (p6) sw r10, x+4

Tabla 17. Optimización del código VLIW del problema 9.

10. En un procesador VLIW cuyas instrucciones pueden codificar dos operaciones (dos campos
o slots en cada instrucción VLIW), todas las operaciones pueden predicarse. Para establecer
los valores de los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

Indique cómo se escribiría la siguiente sentencia sin ninguna operación de salto y con el
mínimo número de instrucciones VLIW, teniendo en cuenta que las instrucciones de
comparación sólo pueden aparecer en el primer campo o slot de la instrucción VLIW (el resto
de las instrucciones pueden aparecer en cualquier campo).

if ((x[0] % 2) == 0)
{
for (i =1 ; i < 3 ; i++)
{
if ((x[i] % 2) == 0)
x[i] = 2 * x[i];
else if (x[0] < 0)
x[i] = 0;
}
}

Solución
Como en el enunciado nos indican que la secuencia de instrucciones debe quedar sin ningún salto,
debemos desenrollar el bucle y predicar las instrucciones que contiene en su interior. El código en
ensamblador derivado del bucle, sería el siguiente:

lw r1, x ; Cargamos x[0]


andi r2, r1, #1 ; Comprobamos si es par

p1 cmp.eq r2, r0 ; p1 = 1 si x[0] es par


p2 cmp.ne r0, r0 ; p2 = 0
p3 cmp.ne r0, r0 ; p3 = 0
p4 cmp.ne r0, r0 ; p4 = 0

(p1) lw r3, x+4 ; r3 = x[1]


(p1) andi r4, r3, #1 ; Comprobamos si es par
(p1) p2, p3 cmp.eq r4, r0 ; p2 = 1 si x[1] es par
(p2) slli r3, r1, #1 ; r3 = 2 * x[1]
(p2) sw r3, x+4 ; x[1] = 2 * x[1]
(p3) p4 cmp.lt r1, r0 ; p4 = 1 si x[0] < 0
(p4) sw r0, x+4 ; x[1] = 0

p5 cmp.ne r0, r0 ; p5 = 0
p6 cmp.ne r0, r0 ; p6 = 0
p7 cmp.ne r0, r0 ; p7 = 0

(p1) lw r5, x+8 ; r5 = x[2]


(p1) andi r6, r5, #1 ; Comprobamos si es par
(p1) p5, p6 cmp.eq r6, r0 ; p5 = 1 si x[2] es par
(p5) slli r5, r1, #1 ; r5 = 2 * x[2]
(p5) sw r5, x+8 ; x[2] = 2 * x[2]
(p6) p7 cmp.lt r1, r0 ; p7 = 1 si x[0] < 0
(p7) sw r0, x+8 ; x[2] = 0

Tras desenrollar e introducir las operaciones con predicados, solo nos queda reorganizar el código
respetando las dependencias de datos para construir las instrucciones VLIW. El resultado se muestra
en la Tabla 18.

# SLOT 1 SLOT 2
1 p2 cmp.ne r0, r0 lw r1, x
2 p3 cmp.ne r0, r0 andi r2, r1, #1
3 p1 cmp.eq r2, r0
4 p4 cmp.ne r0, r0 (p1) lw r3, x+4
5 p5 cmp.ne r0, r0 (p1) lw r5, x+8
6 p6 cmp.ne r0, r0 (p1) andi r4, r3, #1
7 (p1) p2, p3 cmp.eq r4, r0 (p1) andi r6, r5, #1
8 (p1) p5, p6 cmp.eq r6, r0 (p2) slli r3, r1, #1
9 p7 cmp.ne r0, r0 (p5) slli r5, r1, #1
10 (p3) p4 cmp.lt r1, r0 (p2) sw r3, x+4
11 (p6) p7 cmp.lt r1, r0 (p5) sw r5, x+8
12 (p4) sw r0, x+4 (p7) sw r0, x+8
Tabla 18. Optimización del código VLIW del problema 10.

11. Se dispone de un procesador VLIW con dos slots de emisión en el que todas las operaciones
pueden predicarse. Para establecer los valores de los predicados se utilizan instrucciones de
comparación (cmp) con el formato (p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se
comprueba entre los registros rx y ry (lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y
p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La instrucción sólo se ejecuta si el predicado p = 1
(habrá sido establecido por otra instrucción de comparación). También existen instrucciones
de comparación con el formato (p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera
y p1 = 0 si es falsa. Si una instrucción no tiene el predicado (p), se entiende que se ejecuta
siempre.

Indique cómo se escribiría la siguiente sentencia sin ninguna operación de salto y con el
mínimo número de instrucciones VLIW:

for (i = 0 ; i < 2 ; i++)


{
if (x[i] > 0 && x[i] < a)
x[i] = 2 * x[i];
else if (x[i] > 4 || x[i] < b)
x[i]=4;
}

Solución
Como el código está compuesto por un bucle que itera dos veces y se nos dice en el enunciado que no
debe haber instrucciones de salto en la solución, será necesario desenrollar el bucle dos veces. Cada
una de las iteraciones del bucle seguirá el organigrama mostrado en la Figura 9, que nos ayudará a
escribir el código con predicados que se muestra a continuación:

Inicio

Si
¿x[i] > 0?

p1
Si
¿x[i] < a? No

No
p2
Si
¿x[i] > 4?

No
Si
¿x[i] < b?
p3 p4
x[i] = 2 * x[i] x[i] = 4
No

Fin

Figura 9. Organigrama del problema 11.


; Inicializacion
lw r1, a ; r1 = a
lw r2, b ; r2 = b
addi r3, r0, #4 ; r3 = 4

; Primera iteración
lw r4, x ; r4 = x[0]
p1, p2 cmp.gt r4, r0 ; p1 = 1 si x[0] > 0
p3 cmp.ne r0, r0 ; p3 = 0
p4 cmp.ne r0, r0 ; p4 = 0
(p1) p3, p2 cmp.lt r4, r1 ; p3 = 1 si x[0] < a
(p2) p4 cmp.gt r4, r3 ; p4 = 1 si x[0] > 4
(p2) p4 cmp.lt r4, r2 ; p4 = 1 si x[0] < b
(p3) add r4, r4, r4 ; r4 = 2 * x[0]
(p4) add r4, r0, r3 ; r4 = 4
sw r4, x ; x[0] = r4

; Segunda iteración
lw r5, x+4 ; r5 = x[1]
p5, p6 cmp.gt r5, r0 ; p5 = 1 si x[1] > 0
p7 cmp.ne r0, r0 ; p7 = 0
p8 cmp.ne r0, r0 ; p8 = 0
(p5) p7, p6 cmp.lt r5, r1 ; p7 = 1 si x[1] < a
(p6) p8 cmp.gt r5, r3 ; p8 = 1 si x[1] > 4
(p6) p8 cmp.lt r5, r2 ; p8 = 1 si x[1] < b
(p7) add r5, r5, r5 ; r5 = 2 * x[1]
(p8) add r5, r0, r3 ; r5 = 4
sw r5, x+4 ; x[1] = r5

Una vez que está escrito el código, sólo nos falta planificarlo en los dos slots del procesador
cumpliendo las restricciones que nos indican en el enunciado, tal y como muestra la Tabla 19.

# SLOT 1 SLOT 2
1 p3 cmp.ne r0, r0 lw r4, x
2 p1, p2 cmp.gt r4, r0 lw r5, x+4
3 p5 ,p6 cmp.gt r5, r0 lw r1, a
4 p7 cmp.ne r0, r0 lw r2, b
5 (p1) p3, p2 cmp.lt r4, r1 addi r3, r0, #4
6 (p5) p7, p6 cmp.lt r5, r1 (p3) add r4, r4, r4
7 p4 cmp.ne r0, r0 (p7) add r5, r5, r5
8 (p2) p4 cmp.gt r4, r3
9 (p2) p4 cmp.lt r4, r2
10 p8 cmp.ne r0, r0 (p4) add r4, r0, r3
11 (p6) p8 cmp.gt r5, r3 sw r4, x
12 (p6) p8 cmp.lt r5, r2
13 (p8) add r5, r0, r3
14 sw r5, x+4

Tabla 19. Optimización del código VLIW del problema 11.

12. En un procesador VLIW cuyas instrucciones pueden codificar dos operaciones (dos campos
o slots en cada instrucción VLIW), todas las operaciones pueden predicarse. Para establecer
los valores de los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

Indique cómo se escribiría la siguiente sentencia teniendo en cuenta que las instrucciones de
comparación sólo pueden aparecer en el primer campo o slot de la instrucción VLIW, y las
instrucciones de salto sólo pueden aparecer en el segundo (el resto de las instrucciones
pueden aparecer en cualquier campo):

for (i = 0 ; i < n ; i++)


{
if (x[i] < 0)
y[i] = 0;
else
y[i] = x[i] * x[i];
}

El procesador no implementa lógica de bloqueo para asegurarse que los operandos están
disponibles en el momento de emitir las instrucciones, así que tras las instrucciones de carga
de memoria y multiplicación se deberá dejar 1 ciclo (como mínimo) para esperar a que el
resultado esté disponible. El procesador implementa salto retardado, por lo que la siguiente
instrucción VLIW tras un salto siempre se ejecutará independientemente de que se deba
saltar o no. Evite todos los saltos condicionales que no sean de terminación de bucle y
minimice, en la medida de lo posible, el número de slots vacíos en las instrucciones del
programa mediante reorganización de código y desenrollado de bucles.

Solución
La primera aproximación a la solución definitiva consistirá en escribir un programa sencillo que
resuelva el problema. El código en ensamblador derivado del bucle, sería el siguiente:

inicio: lw r1, n ; r1 = n
add r2, r0, r0 ; r2 = 0 (desplazamiento para recorrer x e y)
bucle: lw r3, x(r2) ; r3 = x[i]
p1, p2 cmp.lt r3, r0 ; p1 = 1 si x[i] < 0
(p1) add r4, r3, r0 ; y = 0 si x[i] < 0
(p2) mult r4, r3, r3 ; y = x[i] * x[i] si x[i] ≥ 0
sw r4, y(r2) ; almaceno y(i)
addui r2, r2, #4 ; incremento el desplazamiento
subi r1, r1, #1 ; queda un elemento menos
p3 cmp.gt r1, r0 ; p3 = 1 si quedan elementos
(p3) jmp bucle ; siguiente iteración (si i < n)

Tras escribir el programa en ensamblador, agruparemos las operaciones para crear instrucciones
VLIW para nuestro procesador, respetando que las comparaciones deben ir al slot 1, los saltos al slot 2,
que las multiplicaciones y las cargas de memoria tienen 2 ciclos de latencia, y que se implementa un
salto retardado en el que la siguiente instrucción se ejecuta siempre. El resultado se muestra en la
Tabla 20.

ETIQUETA SLOT 1 SLOT 2


inicio: lw r1, n add r2, r0, r0
bucle: lw r3, x(r2)

p1, p2 cmp.lt r3, r0


(p1) add r4, r3, r0 (p2) mult r4, r3, r3

sw r4, y(r2) addui r2, r2, #4


subi r1, r1, #1
p3 cmp.gt r1, r0
(p3) jmp bucle

Tabla 20. Primera aproximación al código VLIW del problema 12.

Como se puede comprobar, quedan bastantes slots vacíos. Para solucionar el problema, en un primer
paso reorganizaremos el código subiendo el incremento de r2, el decremento de r1 y la comparación
de r1 con r0. Al subir el incremento de r2, habrá que modificar también la operación de
almacenamiento para que almacene el valor de y en la posición correcta. El resultado se muestra en la
Tabla 21.

ETIQUETA SLOT 1 SLOT 2


inicio: lw r1, n add r2, r0, r0
bucle: lw r3, x(r2) addui r2, r2, #4
subi r1, r1, #1
p1, p2 cmp.lt r3, r0
p3 cmp.gt r1, r0 (p2) mult r4, r3, r3
(p1) add r4, r3, r0 (p3) jmp bucle
sw r4, y – 4(r2)

Tabla 21. Segunda aproximación al código VLIW del problema 12.

Con esta modificación hemos conseguido ahorrar cuatro instrucciones y sólo nos quedan tres slots
vacíos, aunque el overhead del bucle es de cuatro operaciones (el incremento de r2, el decremento de r1,
una comparación y un salto) de las doce del cuerpo del bucle, es decir, un 33%. Este overhead se puede
reducir si se desenrolla el bucle, como indica la Tabla 22 (suponemos que n es par). En esta nueva
versión del programa, se ha reducido el número de nops a 2 y el overhead del bucle al 25%.

ETIQUETA SLOT 1 SLOT 2


inicio: lw r1, n add r2, r0, r0
bucle: lw r3, x(r2) lw r13, x + 4(r2)
addui r2, r2, #8 subi r1, r1, #2
p1, p2 cmp.lt r3, r0
p11, p12 cmp.lt r13, r0 (p2) mult r4, r3, r3
p3 cmp.gt r1, r0 (p12) mult r14, r13, r13
(p1) add r4, r3, r0 (p11) add r41, r13, r0
sw r4, y – 8(r2) (p3) jmp bucle
sw r14, y – 4(r2)

Tabla 22. Tercera aproximación al código VLIW del problema 12.

13. Suponga que, en la sentencia condicional siguiente:

if (a == 0)
a = b;
else
a = a + 2;

la variable a es igual a cero la mayoría de la veces. Ilustre cómo el compilador puede mejorar
el tiempo de ejecución utilizando procesamiento especulativo. Realice una estimación de la
ganancia que se obtiene con la especulación en las condiciones establecidas para el
procesador descrito en el problema 1.
Solución
El código ensamblador para esta sentencia, sin utilizar especulación podría ser:

lw r3, a ; r3 = a
bnez r3, else ; Saltamos a else si a != 0

if: lw r3, b ; r3 = b
j fin ; Saltamos a fin para almacenar el resultado

else: addi r3, r3, #2 ; r3 = a + 2

fin: sw r3, a ; Almacenamos el resultado en a

La Tabla 23 muestra la distribución de estas operaciones entre instrucciones VLIW.

ETIQUETA OP1 OP2 OP3 OP4


lw r3, a
bnez r3, else
if lw r3, b j fin
else addi r3, r3, #2
fin sw r3, a

Tabla 23. Código VLIW sin especulación para el problema 13.

En el caso de que la variable a sea igual a cero, y teniendo en cuenta las latencias de las unidades de
ejecución, el código tardaría en ejecutarse 12 ciclos, tal y como muestra la Tabla 24. En cualquier otro
caso, la sentencia se ejecutaría en 9 ciclos, como muestra la Tabla 25.

# OP1 OP2 OP3 OP4


1 lw r3, a
2
3
4 bnez r3, else
5
6
7
8 lw r3, b j fin
9
10
11
12 sw r3, a

Tabla 24. Ejecución de la rama if del código sin especulación para el problema 13.

# OP1 OP2 OP3 OP4


1 lw r3, a
2
3
4 bnez r3, else
5
6
7
8 addi r3, r3, #2
9 sw r3, a

Tabla 25. Ejecución de la rama else del código sin especulación para el problema 13.
Aprovechando que la variable a va a ser igual a cero casi siempre, se podría cargar b especulativamente
antes de la instrucción de salto. De esta forma se evitaría una instrucción de salto y se adelantaría la
instrucción de carga, que además es independiente de la de salto. El código especulativo quedaría:

lw r3, a ; r3 = a
lw r4, b ; r4 = b
beqz r3, fin ; Si a es igual a 0, saltamos a fin para almacenar el resultado
addi r4, r3, #2 ; r4 = a + 2
fin: sw r4, a ; Almacenamos el resultado en a

Como se puede comprobar, se necesita realizar un renombrado del registro r3, utilizando r4, para
conservar el valor cargado de a en r3 cuando se ejecuta la carga de b. En este caso, la distribución de
operaciones en instrucciones VLIW podría ser la que muestra la Tabla 26.

ETIQUETA OP1 OP2 OP3 OP4


lw r3, a lw r4, b
beqz r3, fin
addi r4, r3, #2
fin sw r4, a

Tabla 26. Código VLIW con especulación para el problema 13.

En el caso de que a sea igual a cero, el código especulativo tardaría en ejecutarse 8 ciclos, como indica
la Tabla 27. Para cualquier otro valor de a, el código especulativo tardaría 9 ciclos en ejecutarse, como
indica la Tabla 28.

# OP1 OP2 OP3 OP4


1 lw r3, a lw r4, b
2
3
4 beqz r3, fin
5
6
7
8 sw r4, a

Tabla 27. Ejecución de la rama if del código con especulación para el problema 13.

# OP1 OP2 OP3 OP4


1 lw r3, a lw r4, b
2
3
4 beqz r3, fin
5
6
7
8 addi r4, r3, #2
9 sw r4, a

Tabla 28. Ejecución de la rama else del código con especulación para el problema 13.

En los análisis realizados no se ha tenido en cuenta la posibilidad de que exista predicción de salto.
Asumiendo que p es la probabilidad de que la variable a sea igual a cero, el tiempo de ejecución del
código no especulativo generado para la sentencia if sería:

Tno_espec p   12 p  91  p   3 p  9 ciclos


y el tiempo para el código especulativo:

Tespec  p   8 p  91  p   9  p ciclos

Por tanto, la ganancia en velocidad obtenida será de:

Tno_espec p  3p 9
S p  
Tespec  p  9 p

que, en función del valor de p, estará acotada entre:

S 0  1  S  p   S 1  1.5

Como se asume que la variable a será casi siempre igual a cero, la ganancia tenderá a 1.5. De todas
formas, en el peor de los casos, en el que la variable a nunca tome el valor cero, el tiempo de ejecución
no empeoraría, por lo que esta optimización es aconsejable en cualquier caso, ya que en el peor caso
no empeora y en el resto consigue un tiempo de ejecución menor.

14. Suponga un procesador VLIW que puede codificar tres operaciones por instrucción con las
restricciones que indica la Tabla 29. El procesador puede utilizar predicados con cualquier
instrucción. Aunque en el caso de utilizarlos, una instrucción con la ALU o de acceso a
memoria incrementa en un ciclo su latencia. Esos predicados se establecen a partir de los
resultados de las instrucciones de comparación (cmp) con el formato (p) p1, p2 cmp.cnd rx, ry
donde cnd es la condición que se comprueba entre los registros rx y ry (lt, ge, eq, ne,…). Si la
condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La instrucción sólo se
ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción de comparación).
También existen instrucciones de comparación con el formato (p) p1 cmp.cnd rx, ry donde
p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una instrucción no tiene el
predicado (p), se entiende que se ejecuta siempre.

UNIDAD
LATENCIA OP1 OP2 OP3 OPERACIONES REALIZADAS
FUNCIONAL
Comparaciones, sumas y restas
ALU 1 X X X
con enteros y operaciones lógicas
Operaciones aritméticas con
FP 4 X
números en coma flotante
Memoria 3 X X Cargas y almacenamientos

Saltos condicionales e
Saltos 3 X
incondicionales

Tabla 29. Especificaciones del procesador VLIW del problema 14.

Si sabemos de antemano que el 75% de los elementos del vector a son nulos, muestre, sin
desenrollar el bucle, las formas de ejecutar el código:

void sum(int c[ ], int a[ ], int b[ ], int n)


{
int i;
for (i = 0 ; i < n ; i++)
if (a[i] == 0)
c[i] = b[i];
else
c[i] = a[i] + 1;
}
a) Sin utilizar procesamiento especulativo ni instrucciones con predicados.

b) Utilizando instrucciones con predicado.

c) Utilizando procesamiento especulativo.

d) Utilizando procesamiento especulativo e instrucciones con predicado.

e) Estime los tiempos de ejecución en cada uno de las situaciones anteriores.

Solución
Sin utilizar procesamiento especulativo ni predicados, el código máquina puede ser el siguiente:

sum: lw r1, n ; r1 = Número de elementos


add r2, r0, r0 ; r2 = Desplazamiento de los elementos en los vectores

bucle: lw r3, a(r2) ; r3 = a[i]


bnez r3, else ; Saltamos a else si a[i] != 0

if: lw r4, b(r2) ; r4 = b[i]


j fin

else: addi r4, r3, #1 ; r4 = a[i] + 1

fin: sw r4, c(r2) ; c[i] = r4


addi r2, r2, #4 ; Desplazamiento para el siguiente elemento
subi r1, r1, #1 ; Queda un elemento menos
bnez r1, bucle

Teniendo en cuenta el formato de las instrucciones VLIW, el código sería el que muestra la Tabla 30
(39% del slots vacíos).

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2)
subi r1, r1, #1 bnez r3, else
if lw r4, b(r2) j fin
else addi r4, r3, #1
fin sw r4, c(r2) addi r2, r2, #4 bnez r1, bucle

Tabla 30. Código VLIW sin especulación y sin predicados para el problema 14.

En el caso de que se usen instrucciones con predicado, el código anterior quedaría así:

sum: lw r1, n ; r1 = Número de elementos


add r2, r0, r0 ; r2 = Desplazamiento de los elementos en los vectores

bucle: lw r3, a(r2) ; r3 = a[i]

p1, p2 cmp.eq r3, r0 ; Saltamos a else si a[i] != 0


(p1) lw r4, b(r2) ; r4 = b[i]
(p2) addi r4, r3, #1 ; r4 = a[i] + 1

sw r4, c(r2) ; c[i] = r4


addi r2, r2, #4 ; Desplazamiento para el siguiente elemento
subi r1, r1, #1 ; Queda un elemento menos
bnez r1, bucle

La Tabla 31 muestra el código VLIW de esta aproximación (con un 33% de slots vacíos).
ETIQ. OP1 OP2 OP3
sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2)
p1, p2 cmp.eq r3, r0 subi r1, r1, #1 bnez r1, bucle
(p1) lw r4, b(r2) (p2) addi r4, r3, #1
sw r4, c(r2) addi r2, r2, #4

Tabla 31. Código VLIW sin especulación y con predicados para el problema 14.

Aprovechando que la mayoría de los elementos del vector a son nulos, podemos hacer uso del
siguiente código especulativo:

sum: lw r1, n ; r1 = Número de elementos


add r2, r0, r0 ; r2 = Desplazamiento de los elementos en los vectores

bucle: lw r3, a(r2) ; r3 = a[i]


lw.s r4, b(r2) ; r4 = b[i] (especulativamente)
beqz r3, fin ; Saltamos a fin si a[i] != 0

addi r4, r3, #1 ; r4 = a[i] + 1

fin: sw r4, c(r2) ; c[i] = r4


addi r2, r2, #4 ; Desplazamiento para el siguiente elemento
subi r1, r1, #1 ; Queda un elemento menos
bnez r1, bucle

En este código se ha supuesto que el procesador incorpora una instrucción de carga especulativa (lw.s)
que se implementa mediante el uso de bits de veneno (poison bits). Si la carga especulativa fallara, se
envenenaría el registro r4 y cualquier otro registro que operara con él. Si al final se intentara almacenar
un registro envenenado, se atendería la excepción. En este caso, si fallara la carga especulativa de b y la
variable a fuera igual a cero, se atendería la excepción al intentar almacenar el registro r4. Sin embargo,
si fallara la carga especulativa de b, y a fuera distinta e cero, se volvería a fijar un valor nuevo para r4
(no envenenado), por lo que no sería necesario atender la excepción. La Tabla 32 muestra el código
VLIW para esta versión especulativa (con un 33% de slots vacíos).

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2) lw.s r4, b(r2)
subi r1, r1, #1 beqz r3, fin
addi r4, r3, #1
fin sw r4, c(r2) addi r2, r2, #4 bnez r1, bucle

Tabla 32. Código VLIW con especulación y sin predicados para el problema 14.

Por último, se pueden ahorrar la mitad de los saltos del programa si, además de especulación, se
utilizan instrucciones con predicado. En este caso, se obtendría el siguiente código. La Tabla 33
muestra su versión VLIW (con un 33% de slots vacíos):

sum: lw r1, n ; r1 = número de elementos


add r2, r0, r0 ; r2 = desplazamiento de los elementos en los vectores

bucle: lw r3, a(r2) ; r3 = a[i]


lw.s r4, b(r2) ; r4 = b[i] (especulativamente)
p1 cmp.eq r3, r0 ; Saltamos a fin si a[i] != 0
(p1) addi r4, r3, #1 ; r4 = a[i] + 1
sw r4, c(r2) ; c[i] = r4
addi r2, r2, #4 ; desplazamiento para el siguiente elemento
subi r1, r1, #1 ; queda un elemento menos
bnez r1, bucle

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2) lw.s r4, b(r2)
p1 cmp.eq r3, r0 subi r1, r1, #1 bnez r1, bucle
(p1) addi r4, r3, #1
sw r4, c(r2) addi r2, r2, #4

Tabla 33. Código VLIW con especulación y con predicados para el problema 14.

Una vez obtenidos los códigos VLIW para cada una de las situaciones propuestas en el problema,
pasamos a estimar el tiempo de ejecución de cada una de ellas. Para el primer caso, tenemos que
evaluar los tiempos de ejecución del programa para las dos partes del if. Si a[i] es cero, se tendría la
ejecución mostrada en la Tabla 34, mientras que en cualquier otro caso, se obtendrían la que muestra
la Tabla 35. Asumiendo que la probabilidad de que a[i] = 0 es p = 0.75, el tiempo medio de ejecución
será:

T1 n   p  12n  1  1  p  10n  1  11.5n  1 ciclos

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2)

subi r1, r1, #1 bnez r3, else

if lw r4, b(r2) j fin

fin sw r4, c(r2) addi r2, r2, #4 bnez r1, bucle

Tabla 34. Ejecución del código de la Tabla 30 suponiendo que a[i] es cero.

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2)

subi r1, r1, #1 bnez r3, else

else addi r4, r3, #1


fin sw r4, c(r2) addi r2, r2, #4 bnez r1, bucle

Tabla 35. Ejecución del código de la Tabla 30 suponiendo que a[i] no es cero.

En el caso en el que se usen instrucciones con predicado para evitar el salto condicional, la ejecución
sería como indica la Tabla 36, así que el tiempo medio de ejecución será, independientemente de p:
T2 n   7n  1 ciclos

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2)

p1, p2 cmp.eq r3, r0 subi r1, r1, #1 bnez r1, bucle


(p1) lw r4, b(r2) (p2) addi r4, r3, #1

sw r4, c(r2) addi r2, r2, #4

Tabla 36. Ejecución del código de la Tabla 31.

Para el código de la tercera opción, en el que se permite usar accesos especulativos a memoria, hay que
volver a tener en cuenta el tiempo de ejecución de las dos ramas del if. La Tabla 37 muestra la
ejecución en el caso de que a[i] sea cero y la Tabla 38 en el caso contrario. A partir de estas dos tablas,
y asumiendo que la probabilidad de que a[i] sea nulo es del 75%, el tiempo medio de ejecución será:

T3 n   p  9n  1  1  p  10n  1  9.25n  1 ciclos

Como el código especulativo se ejecuta en la mayoría de los casos, se ha conseguido reducir el tiempo
medio de ejecución en más de dos ciclos por iteración respecto al tiempo original, aunque no se llega a
superar el tiempo alcanzado al usar instrucciones con predicado debido a la alta latencia de los saltos.

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2) lw.s r4, b(r2)

subi r1, r1, #1 beqz r3, fin

fin sw r4, c(r2) addi r2, r2, #4 bnez r1, bucle

Tabla 37. Ejecución del código de la Tabla 32 suponiendo que a[i] es cero.

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2) lw.s r4, b(r2)

subi r1, r1, #1 beqz r3, fin

addi r4, r3, #1


fin sw r4, c(r2) addi r2, r2, #4 bnez r1, bucle

Tabla 38. Ejecución del código de la Tabla 32 suponiendo que a[i] no es cero.
Por último, analizaremos el tempo de ejecución del caso en el que se aplica tanto especulación como
instrucciones con predicado. La Tabla 39 muestra cómo se ejecutaría el código, tardando una media
de:

T4 n   7n  1 ciclos

ETIQ. OP1 OP2 OP3


sum lw r1, n add r2, r0, r0
bucle lw r3, a(r2) lw.s r4, b(r2)

p1 cmp.eq r3, r0 subi r1, r1, #1 bnez r1, bucle


(p1) addi r4, r3, #1

sw r4, c(r2) addi r2, r2, #4

Tabla 39. Ejecución del código de la Tabla 33.

Como se puede ver, la situación más favorable corresponde al uso de predicados. En este caso hemos
obtenido que T4(n) = T2(n) porque la latencia de la carga de b se ha solapado con la del salto del bucle.
En otros casos más complejos deberían obtenerse mejores resultados usando ambas técnicas
simultáneamente. Si no se usan instrucciones con predicado, el uso de cargas especulativas favorece
también el tiempo de ejecución, siempre que se especule sobre las instrucciones cuya ejecución sea
más probable.

15. El siguiente fragmento de código forma parte de una rutina que se ejecuta muy a menudo en
cierto procesador VLIW:

mult r1, r2, r3


add r4, r5, r1
beqz r4, cero
lw r6, dato
add r7, r4, r6
mult r2, r1, r7
cero: sub r8, r1, r4
mult r1, r8, r2
sw resul, r1

En este procesador se pueden predicar todas las operaciones. Para establecer los valores de
los predicados se utilizan instrucciones de comparación (cmp) con el formato
(p) p1, p2 cmp.cnd rx, ry donde cnd es la condición que se comprueba entre los registros rx y ry
(lt, ge, eq, ne,…). Si la condición es verdadera p1 = 1 y p2 = 0, y si es falsa, p1 = 0 y p2 = 1. La
instrucción sólo se ejecuta si el predicado p = 1 (habrá sido establecido por otra instrucción
de comparación). También existen instrucciones de comparación con el formato
(p) p1 cmp.cnd rx, ry donde p1 = 1 si la condición es verdadera y p1 = 0 si es falsa. Si una
instrucción no tiene el predicado (p), se entiende que se ejecuta siempre.

La arquitectura VLIW tiene dos slots, pero las instrucciones de comparación sólo pueden
colocarse en el primero de ellos. Las latencias de las operaciones son de 1 ciclo para las
sumas, restas y comparaciones, de dos ciclos para las multiplicaciones y de cinco ciclos para
las cargas de memoria. Debido a esta alta latencia en el acceso a memoria, la arquitectura
incorpora una instrucción de carga especulativa de memoria, lw.s rx, desp(ry), repar, que
permite cargar un dato anticipadamente. Para dar soporte a esta instrucción, los registros del
procesador cuentan con un bit de veneno adicional para marcar aquellos resultados en los
que la especulación haya provocado una excepción. La aplicación de cualquier operación con
un operando envenenado provocará que se envenene el resultado, y la excepción sólo se atenderá
si se intenta almacenar un resultado envenenado. En dicho caso, tras atender la excepción se
ejecutará el código de reparación indicado con el puntero repar.

Optimice el código anterior de manera que no aparezca ninguna instrucción de salto, se


tengan en cuenta las latencias de las operaciones y los slots a los que se pueden emitir, y se
adelante la instrucción de carga especulativamente todo lo que sea posible para tratar de
ocultar su latencia.

Solución
Lo primero que vamos a hacer es escribir el programa usando predicados:

mult r1, r2, r3


add r4, r5, r1
p1 cmp.ne r4, r0
(p1) lw r6, dato
(p1) add r7, r4, r6
(p1) mult r2, r1, r7
sub r8, r1, r4
mult r1, r8, r2
sw resul, r1

Si empaquetamos estas instrucciones en instrucciones VLIW para el procesador descrito en el


enunciado, respetando los tipos de instrucciones que se pueden emitir en cada slot y las latencias de las
instrucciones, comprobamos que el programa tarda en ejecutarse 15 ciclos, de los cuales el procesador
está cuatro ocioso debido a la alta latencia de la carga de memoria, tal y como muestra la Tabla 40.

#. OP1 OP2
1 mult r1, r2, r3
2
3 add r4, r5, r1
4 p1 cmp.ne r4, r0 sub r8, r1, r4
5 (p1) lw r6, dato
6
7
8
9
10 (p1) add r7, r4, r6
11 (p1) mult r2, r1, r7
12
13 mult r1, r8, r2
14
15 sw resul, r1

Tabla 40. Código VLIW sin especulación para el problema 15.

Sería interesante que la carga se realizara al principio del programa para tratar de ocultar esta latencia.
Como está vigilada por p1, se debe hacer especulativamente para ignorar las posibles excepciones que
puedan ocurrir en su procesamiento, ya que si al calcular r4 toma el valor 0, la carga no debería
haberse realizado y por tanto, no debería haber ocurrido ninguna excepción. El código para adelantar
la carga es el siguiente:
lw.s r6, dato, repar
mult r1, r2, r3
add r4, r5, r1
p1 cmp.ne r4, r0
(p1) add r7, r4, r6
(p1) mult r2, r1, r7
sub r8, r1, r4
mult r1, r8, r2
sw resul, r1

Y una vez colocado en forma de instrucciones VLIW, la Tabla 41 muestra que tarda 4 ciclos menos en
ejecutarse, siempre que no falle la carga especulativa, o que no se use el valor envenenado en caso de
que falle (si p1 toma el valor 0):

#. OP1 OP2
1 mult r1, r2, r3 lw.s r6, dato, repar
2
3 add r4, r5, r1
4 p1 cmp.ne r4, r0 sub r8, r1, r4
5
6 (p1) add r7, r4, r6
7 (p1) mult r2, r1, r7
8
9 mult r1, r8, r2
10
11 sw resul, r1

Tabla 41. Código VLIW con especulación para el problema 15.

Sin embargo, si la carga especulativa falla y además p1 toma el valor 1 en la comparación, r6


envenenaría a r7, r7 a r2, y r2 a r1, por lo que al ejecutarse la instrucción de almacenamiento se
intentaría almacenar un resultado envenenado, permitiendo detectar al procesador que se produjo una
excepción en la carga especulativa. En este punto, el sistema atendería la excepción y después saltaría
automáticamente a la rutina de reparación apuntada por repar, que tendría que recalcular todos los
resultados envenenados hasta que se ha atendido la excepción:

repar: lw r6, dato


add r7, r4, r6
mult r2, r1, r7
mult r1, r8, r2
sw resul, r1

La Tabla 42 muestra el código VLIW para la rutina de reparación. Como se puede observar, la rutina
de reparación tarda en ejecutarse otros 11 ciclos debido a la latencia de la carga de memoria, por lo
que cuando haya un fallo de página el programa tardará en ejecutarse 22 ciclos, que es más tiempo del
que tardaba sin aplicar la optimización. Por tanto, esta optimización debería aplicarse sólo si la
probabilidad de que se produzca un fallo de página al cargar el dato es baja, o bien si la probabilidad
de que r4 tome el valor 0 es pequeña. Estas probabilidades se pueden estimar realizando algunas
ejecuciones del programa sin optimizar y analizando su comportamiento.

#. OP1 OP2
1 lw r6, dato
2
3
4
5
6 add r7, r4, r6
7 mult r2, r1, r7
8
9 mult r1, r8, r2
10
11 sw resul, r1

Tabla 42. Rutina de reparación para el código VLIW con especulación para el problema 15.

También podría gustarte