Capítulo 5

Descargar como doc, pdf o txt
Descargar como doc, pdf o txt
Está en la página 1de 46

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
instruccione
(IF) s

Decodificac
ión (ID)

Slot de Slot de Slot de


emisión emisión emisión

Unidad Unidad Unidad Ejecució


funcional funcional funcional n

(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)
addf4, f2, f0 ld f2, x+8(r3)
sd f4, x(r3) addf4, f2, f0 ld f2, x+16(r3)
sd f4, x+8(r3) addf4, 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
prolo
ld f2, x(r3)
go

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
epilo
add f4, f2, f0 sd f4, x+8(r3)
go

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;

Inic
io

Si No
¿a ≠ 0?

p1
Si No
¿b ≠ 0?

p
Si No
2
¿c ≠ 0?
p p
p
1 4
jj+1 3 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, p2cmp.ne r1, r0 ; p1 = 1 si a != 0 (p2 = 1 en caso
contrario)
(p1) lw r2, b ; r2 = b (si a != 0)
(p1) p1, p2cmp.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, p4cmp.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
p1, p2 cmp.ne
3
r1, r0
addi r6, r6,
4 (p1) lw r2, b
#1
5 sw r6, i
(p1) p1, p2 cmp.ne
6
r2, r0
(p2) p3 cmp.ne
7 (p1) lw r4, j
r0, r0
(p2) p4 cmp.ne
8 (p2) lw r3, c
r0, r0
(p1) addi r4, r4,
9 (p2) lw r5, k
#1
(p2) p3, p4 cmp.ne
10 (p1) sw r4, j
r3, r0
(p3) addi r5, r5, (p4) subi r5, r5,
11
#1 #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
add r3, r4,
2
r5
beqz r10, add r6, r3,
3
loop r7
lw r8,
4
0(r10)
5
lw r9,
6
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
p1 cmp.ne
1 lw r1, a
r10, r0
(p1) lw r8, add r3, r4,
2
0(r10) r5
beqz r10, add r6, r3,
3
loop r7
lw r9,
4
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
lw r12,
add r5, r3, r4
a+12(r2)
r13,b+12(r2)
sw r5, c(r2) bnez r1, ,inicio add r8, r6, r7
sw r8, c+4(r2) add r11, r9, r10
sw r11, add r14, r12,
c+8(r2) r13
sw r14,
addi r2, r2, #16
c+12(r2)

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 LATENCI


GENERA EL RESULTADO A
(CICLOS)
Operación en coma 3
flotante
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,
muld f6, f2, f4
y+8(r2)
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
Smax  limS 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
add r10, r11,
1 lw r1, x(r2)
r12
add r13, r10,
2
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
add r10, r11,
1 lw r1, x(r2)
r12
add r13, r10,
2 lwc r4, 0(r3), r3
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 ≥
p1
b?
Si No
¿b ≥
p2
p1 0?
Si No
x←1 ¿b <
p2
0?
Si No
¿a <
p2 b?
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
add r3, r0,
2
r0
3
p1, p2 cmp.ge
4
r1, r2
(p1) p1, p2 cmp.ge
5
r2, r0
6 (p2) p2 cmp.lt r2, r0
7 (p2) p2 cmp.lt r1, r2
(p1) addi r3, r0, (p2) addi r3, r0,
8
#1 #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.

Inic
io

Si No
¿a > b?
p1
Si No
¿a > c?
p
Si 2 No
¿a < b?
p
p Si No
4
3 ¿a < c?
x2*x p
5
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
p3 cmp.ne p4 cmp.ne p5 cmp.ne
2
r0, r0 r0, r0 r0, r0
3 p1, p2 cmp.gtr2, r3 lw r4, c
4
5 (p1) p3, p2 cmp.gtr2, r4 lw r5, x
6
(p3) slli r5, r5,
7 (p2) p4 cmp.lt r2, r3
#1
8
9 (p4) p5 cmp.lt r2, r4
10
(p5) slli r5, r5,
11
#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.
Inici
o
Si No
¿a >
b?
p
Si 2 No
¿a >
c?
p p
1 Si No
xx*2 3
¿a <
b?
p
Si No
4
¿a >
p c?
5
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


p3 cmp.ne
1 lw r2, a lw r3, b
r0, r0
2 p1,p2 cmp.gtr2, r3 lw r4, c lw r5, x
p4 cmp.ne (p1) slli r5, r5,
3 (p2) p1,p3 cmp.gtr2, r4
r0, r0 #1
p5 cmp.ne
4 (p3) p4 cmp.lt r2, r3
r0, r0
5 (p4) p5 cmp.lt r2, r4
(p5) srai r5, r5,
6
#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
p ¿x > 3?
p2
1 Si
yx N
¿x > 0?
o
xx/2
p
N 3 Si
¿x < 1?
o
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


p3 cmp.ne addi r1, r0, addi r3, r0,
1
r0, r0 #1 #3
p4 cmp.ne
2 lw r5, x
r0, r0
3 p1, p2 cmp.gtr5, r3
(p1) sra r6, r5,
4 (p2) p3 cmp.gtr5, r0 (p1) sw r5, y
r1
5 (p3) p4 cmp.lt r5, r1 (p1) sw r6, x
(p4) sub r7, r0, (p4) sll r8, r5,
6
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
p ¿x[0] >
2? p
1y[0]  x [0] Si No
p ¿x [0] > 2
x[0]  3 * x [0] 3
y[0]  – x [0] 0?

x [0]  5 * x [0]

Si No
p ¿x [1] >
p
4y [1]  x [1] 2?
Si 5 No
p ¿x [1] >
x [1]  3 * x [1] 6
y[1]  – x [1] 0?

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
p3 cmp.ne
1 lw r1, x lw r6, x+4
r0, r0
p6 cmp.ne subi r2, r1, subi r7, r6,
2
r0, r0 #2 #2
addi r13, r0, addi r15, r0,
3 p1, p2 cmp.gtr2, r0
#3 #5
(p1) mult r3, r1,
4 p4, p5 cmp.gtr7, r0 (p1) sw r1, y
r13
(p4) mult r8, r6,
5 (p2) p3 cmp.gtr1, r0 (p4) sw r6, y+4
r13
6 (p5) p6 cmp.gtr6, r0 (p1) sw r3, x (p4) sw r8, x+4
(p3) sub r4, r0, (p6) sub r9, r0, (p3) mult r5, r1,
7
r1 r6 r15
(p6) mult r10, r6,
8 (p3) sw r4, y (p6) sw r9, y+4
r15
(p6) sw r10,
9 (p3) sw r5, x
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?
p
Si 1
¿x[i] No
< a?
No
p2
Si
¿x[i]
> 4?
No
Si
¿x[i] <
p3 b? p4
x[i] = 2 * x[i] = 4
No
x[i]

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
p3 cmp.ne
1 lw r4, x
r0, r0
2 p1, p2 cmp.gtr4, r0 lw r5, x+4
3 p5 ,p6 cmp.gtr5, r0 lw r1, a
p7 cmp.ne
4 lw r2, b
r0, r0
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
p4 cmp.ne
7 (p7) add r5, r5, r5
r0, r0
8 (p2) p4 cmp.gtr4, r3
9 (p2) p4 cmp.lt r4, r2
p8 cmp.ne
10 (p4) add r4, r0, r3
r0, r0
11 (p6) p8 cmp.gtr5, 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.gtr1, 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.gtr1, 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
lw r13, x +
bucle: lw r3, x(r2)
4(r2)
addui r2, r2, #8 subi r1, r1, #2
p1, p2 cmp.lt r3, r0
p11, p12 cmp.lt r13,
(p2) mult r4, r3, r3
r0
(p12) mult r14,
p3 cmp.gtr1, r0
r13, r13
(p11) add r41,
(p1) add r4, r3, r0
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:

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


Tno_espec

y el tiempo para el código especulativo:

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

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

 p
Tno_espec 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


add r2, r0,
sum lw r1, n
r0
lw r3,
bucle
a(r2)
p1, p2 cmp.eq subi r1, r1, bnez r1,
r3, r0 #1 bucle
(p1) lw r4, (p2) addi r4, r3,
b(r2) #1
sw r4, addi r2, r2,
c(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


add r2, r0,
sum lw r1, n
r0
lw r3, lw.s r4,
bucle
a(r2) b(r2)
p1 cmp.eq subi r1, r1, bnez r1,
r3, r0 #1 bucle
(p1) addi r4, r3,
#1
sw r4, addi r2, r2,
c(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 1ciclos

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 1ciclos

ETIQ. OP1 OP2 OP3


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

p1, p2 cmp.eq subi r1, r1, bnez r1,


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

sw r4, addi r2, r2,


c(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 1ciclos

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 1ciclos

ETIQ. OP1 OP2 OP3


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

p1 cmp.eq subi r1, r1, bnez r1,


r3, r0 #1 bucle
(p1) addi r4, r3,
#1

sw r4, addi r2, r2,


c(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
p1 cmp.ne r4,
4 sub r8, r1, r4
r0
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
lw.s r6, dato,
1 mult r1, r2, r3
repar
2
3 add r4, r5, r1
p1 cmp.ne r4,
4 sub r8, r1, r4
r0
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