Enunciado y soluciones del final

Anuncio
Arquitectura e Ingenierı́a de Computadores
Examen final de junio
Jueves, 19 de junio de 2008
¡Acuérdate de poner tu nombre en todas las hojas que utilices!
¡Justifica claramente todas tus contestaciones!
Utiliza grupos de folios separados para responder a las cuestiones de cada parte
SOLUCIONES
PARTE ARQUITECTURAS MONOPROCESADOR
1. (2 puntos) Responder verdadero o falso a las siguientes cuestiones justificando adecuadamente
las respuestas.
a) (0,4 puntos) La planificación estática de instrucciones ayudada por una optimización basada
en la búsqueda de paralelismo por parte del compilador, es una técnica común empleada en los
procesadores superescalares actuales.
b) (0,4 puntos) En una máquina superescalar capaz de lanzar dos instrucciones por ciclo (1 de
punto fijo y 1 de punto flotante), con planificación dinámica mediante Tomasulo y especulación,
la presencia de riesgos WAW puede impedir el lanzamiento de dos instrucciones en un ciclo.
c) (0,4 puntos) Tanto los procesadores superescalares como los procesadores VLIW dependen mucho de una adecuada predicción de los saltos de la aplicación.
d) (0,4 puntos) En las arquitecturas VLIW el procesador recibe el código libre de dependencias y
optimizado para la ejecución paralela.
e) (0,4 puntos) En una arquitectura de tipo VLIW, las caracterı́sticas de la arquitectura hacen
que la complejidad del banco de registros sea muy baja.
Solución
a) Falso. Los procesadores superescalares actuales se basan en la planificación dinámica de código,
la especulación y tener implementado en hardware un algoritmo tipo “Tomasulo” para conseguir
una gran eficiencia.
b) Falso. Una máquina superescalar con especulación no presenta detenciones por riesgos de datos
en el lanzamiento de instrucciones. Concretamente, los riesgos WAW son resueltos por medio
del renombramiento implı́cito de registros que se realiza gracias a Tomasulo.
c) Verdadero. La predicción de los saltos es la base para un buen funcionamiento de los procesadores actuales, ya sean VLIW o superescalares. Lo que cambia en ellos es la manera de realizar
dicha predicción: de forma dinámica, en los superescalares, de forma estática en los procesadores
VLIW.
1
d) Verdadero. Los procesadores VLIW no incorporan chequeo de dependencias, por lo que es
misión del compilador preparar el código objeto para que esté libre de dependencias y optimizado
para la ejecución en una determinada arquitectura VLIW.
e) Falso. Todo lo contrario. Por las caracterı́sticas del diseño VLIW, donde se trata de que en el
mismo ciclo de reloj se acceda al banco de registros por varias instrucciones, el diseño del banco
de registros es más complejo, siendo éste habitualmente un banco de registros partido en varios
sub-bancos.
¤
2. (2 puntos) Tenemos un procesador superescalar homogéneo de grado 2 (en todas las etapas
se pueden manejar hasta 2 instrucciones por ciclo), con búsqueda alineada y emisión alineada y en
orden. Para mantener la consistencia secuencial, ası́ como para renombrar los registros y gestionar
las interrupciones se utiliza un ROB. Existe una estación de reserva para cada unidad de ejecución
y el predictor de saltos es estático y predice que no va a saltar en los saltos hacia adelante, y
que si va a saltar en los saltos hacia atrás. Se dispone de las siguientes unidades de ejecución: dos
unidades de carga/almacenamiento (segmentadas) de latencia 2 (calcula la dirección en el primer
ciclo y accede a la cache en el segundo ciclo), dos ALUs enteras de latencia 1, un sumador en
coma flotante (segmentado) de latencia 3, un divisor en coma flotante (segmentado) de latencia 4,
y una unidad de saltos que resuelve los saltos en la etapa de ejecución. Suponemos que en el cauce
de enteros están implementados los cortocircuitos y reenvı́os habituales, pero no ası́ en el cauce
de coma flotante. Supón para este problema un tamaño ilimitado del ROB y de cada una de las
estaciones de reserva.
En dicho ordenador se va a ejecutar la siguiente secuencia de instrucciones que calcula la división
de dos vectores x e y componente a componente:
lw r3, n
slli r4, r3, #2
inicio: lf f0, x(r2)
lf f1, y(r2)
divf f2, f0, f1
sf f2, z(r2)
addui r2, r2, #4
sub r5, r4, r2
bnez r5, inicio
trap #0
<sgte.>
;
;
;
;
;
;
;
;
;
;
;
;
el valor inicial de n es bastante grande
r3 = n
r4 = n * 4 (final del vector)
f0 = x(i)
f1 = y(i)
f2 = x(i) / y(i)
z(i) = x(i) / y(i)
r2 = r2 + 4 (incremento el desplazamiento)
compruebo si he llegado al final
saltar a inicio si r5 es distinto de 0
fin del programa
suponemos las instrucciones siguientes enteras
Nota: La instrucción trap no causa la terminación del programa hasta que se retira del ROB.
Considera que la primera vez que se accede a memoria (para obtener el valor de n y para obtener
el valor de los vectores a partir de r2 ) se produce un fallo de cache L1 que se resuelve en la cache
L2 con una latencia total de 3 ciclos en el acceso a memoria. El resto de accesos a memoria son
servidos por la cache L1 gracias a la técnica del prefetching.
a) (1 punto) ¿En qué ciclo se confirma la primera instrucción de salto? ¿En que ciclo se confirma la
segunda instrucción de salto? Dibuja el diagrama correspondiente para justificar tu respuesta.
En dicho diagrama indica, para cada instrucción y ciclo de reloj, qué fase de la instrucción se
está ejecutando.
b) (0,5 puntos) ¿Cuántas instrucciones de las dos primeras iteraciones tendrá el ROB al acabar
el ciclo en que se confirma la instrucción (addui r2, r2, #4) de la primera iteración? Para
justificar la respuesta, muestra el contenido del ROB en dicho ciclo.
c) (0,5 puntos) Considera ahora que el procesador tiene un reloj de 1 GHz. y que el régimen
estacionario de ejecución es igual al de la segunda iteración. ¿En cuanto tiempo (en segundos)
2
se ejecutarı́a el código anterior para un tamaño del vector de 1000 elementos? No hace falta que
tengas en cuenta el tiempo necesario para el llenado/vaciado del pipeline. ¿Cuantos MFLOPs
obtendrı́amos para el procesador descrito ejecutando el código anterior?
Solución
a) Para obtener las dos preguntas de este apartado tenemos que simular la ejecución del programa
durante las dos primeras iteraciones. Obtenemos el siguiente diagrama instrucciones–tiempo:
1
lw r3, n
slli r4, r3, #2
lf f0, x(r2)
lf f1, y(r2)
divf f2, f0, f1
sf f2, z(r2)
addui r2, r2, #4
sub r5, r4, r2
bnez r5, inicio
trap #0
lf f0, x(r2)
lf f1, y(r2)
divf f2, f0, f1
sf f2, z(r2)
addui r2, r2, #4
sub r5, r4, r2
bnez r5, inicio
trap #0
lf f0, x(r2)
lf f1, y(r2)
2
3
IF IS EX
IF IS IF IS
IF IS
IF
IF
4
5
6
7
8
9
10 11 12 13 14 15 16 17 18 19 20 21
L2
EX
EX
IS
IS
IF
IF
L2
L2
EX
IS
IS
IF
IF
L2
L2
EX
IS
XX
IF
IF
WB
EX
L2
L2
WB
EX
-
C
WB
WB
L2
WB
EX
C
C
L2
-
WB
-
C
EX
-
EX
-
EX
-
EX
-
WB
-
C
C(S1)
- C
- C
- - C
IS
IS
IF
IF
EX
EX
IS
IS
IF
IF
WB
L1
EX
IS
XX
IF
IF
WB
WB
EX
-
EX
WB
EX
EX
-
EX
-
EX
-
WB
-
L1
EX
IS
IS
IF
IF
-
C
-
C
C
-
C(S1)
C
- C
- C
En el diagrama denotamos con L1 un acceso a memoria resuelto en la cache L1 y con L2 un
acceso a memoria satisfecho por la cache L2. Como podemos apreciar, los tres primeros accesos
a cache fallan en la L1, siendo resueltos en tres ciclos por la cache L2.
A partir del diagrama anterior vemos que la primera instrucción de salto se confirma en el ciclo
18, mientras que la segunda instrucción de salto lo hace en el ciclo 21.
b) La instrucción addui r2, r2, #4 de la primera iteración ejecutada se confirma en el ciclo 17.
Al final de dicho ciclo el contenido del ROB (sólo instrucciones de las 2 primeras iteraciones) es
el siguiente:
Entrada
1
2
3
4
5
6
7
8
ocupado
Si
Si
Si
Si
Si
Si
Si
Si
instr.
bnez r5, inicio
lf f0, x(r2)
lf f1, y(r2)
divf f2, f0, f1
sf f2, z(r2)
addui r2, r2, #4
sub r5, r4, r2
bnez r5, inicio
estado
EX
WB
WB
WB
EX
WB
WB
EX
dest.
valor
f0
f1
f2
Mem[z(r2)]
r2
r5
Mem[x(r2)]
Mem[y(r2)]
#2 / #3
r2+4
r4 - #6
El número total de entradas del ROB ocupadas es de 8 al final del ciclo 17. Como se puede ver,
hemos rellenado el campo valor para todas aquellas entradas que ya han pasado por su etapa
WB.
3
c) Del diagrama del apartado a) podemos ver que cada iteración del bucle en el régimen estacionario tarda en ejecutarse 4 ciclos de reloj (medido como la diferencia entre el comienzo de las
instrucciones de carga en el régimen normal). Por lo tanto, el tiempo que tardan en ejecutarse
1000 iteraciones, sin tener en cuenta los ciclos del llenado/vaciado del pipeline, es el siguiente:
T iempo = 4 ciclos ∗ 1000 = 4000 ciclos = 4 µseg
Para el cálculo de los MFLOPs, tenemos que en cada iteración sólo realizamos una operación
en coma flotante (la división), por lo que obtendrı́amos:
MFLOPs = 1 op / 4 ciclos @ 1 Ghz = 250 MFLOPs.
¤
PARTE ARQUITECTURAS MULTIPROCESADOR
3.
(3 puntos) Responder brevemente a cada una de las cuestiones teórico/prácticas que a
continuación se plantean (se valorará la capacidad de concreción del alumno).
a) (0,25 puntos) Dada una aplicación cientı́fica que requiere la realización de 2 × 1015 operaciones
de punto flotante, y dado que disponemos de procesadores con 25 GFLOPS, ¿cuál es el número
mı́nimo teórico de procesadores que necesitarı́amos en nuestra máquina paralela para poder
ejecutar la aplicación en 1 hora?¿Crees que dicho número de procesadores coincidirá con el que
se necesitarán en la práctica?
b) (0,25 puntos) ¿Por qué podemos argumentar que la Ley de Amdahl da una visión un tanto
pesimista de las ventajas del paralelismo?
c) (0,25 puntos) ¿Cuál de las dos organizaciones de directorio plano (basado en memoria o basado
en cache) es más escalable desde el punto de vista del rendimiento?
d) (0,25 puntos) ¿En qué consiste la compartición falsa (false sharing)? Desde el punto de vista
del programador, ¿cómo crees que se podrı́a evitar este fenómeno?
e) (0,25 puntos) Explica en qué caso serı́a preferible realizar la transición de estado M −
→ I frente
a la M −
→ S ante la ocurrencia de un fallo de lectura en un procesador remoto. Pon un ejemplo
de código (preferiblemente que aparezca en la práctica 3) en el que se dé dicha situación.
f) (0,50 puntos) Supón un CMP con 4 núcleos de ejecución. Cada núcleo tiene caches separadas
de primer nivel para datos (16 KB) e instrucciones (8 KB), mientras que la cache de segundo
nivel de 2 MB es compartida entre los 4 núcleos. La cache L2 contiene todos los datos que se
almacenan en las caches L1 (se mantiene la inclusividad). La red de interconexión dentro del
CMP es un bus común y para el mantenimiento de la coherencia de las caches L1 se emplea
un protocolo basado en fisgoneo con los estados MOSI. Para la siguiente secuencia de eventos
sobre el mismo bloque de datos, indica en cada caso las transacciones de bus que se generan, el
vector de estados para el bloque de datos y quién proporciona el bloque de datos (memoria, L2
o L1 remota). Supón que inicialmente las caches están vacı́as.
Lect(core 1) → Escr(core 1) → Lect(core 2) → Lect(core 3) → Escr(core 4) → Lect(core 1) →
reemplazo(core 4) → Lect(core 2)
g) (0,25 puntos) ¿Qué diferencias hay entre el modelo de consistencia secuencial y el modelo de
consistencia débil estudiados en clase? Dado el siguiente fragmento de código, ¿qué resultados
son posibles bajo cada uno de los dos modelos? Suponer que inicialmente el valor de A es 0.
P1
P2
A = 1;
BARRIER(b);
BARRIER(b); {
print A;
4
h) (0,25 puntos) Explicar la principal diferencia entre la versión de los cerrojos basada en las
instrucciones LL-SC y la basada en tickets estudiadas en clase.
i) (0,25 puntos) Explicar la implementación software centralizada de las barreras con cambio de
sentido estudiada en clase. Con respecto a la implementación software centralizada original,
¿qué problema trata de resolver?
j) (0,25 puntos) ¿Qué diferencia hay entre las técnicas de conmutación virtual cut-through y wormhole?
k) (0,25 puntos) Pon un ejemplo de algoritmo de encaminamiento determinista para mallas ndimensionales y explica su funcionamiento.
Solución
a) El número mı́nimo de procesadores que necesitamos (N ) lo calculamos a partir de la siguiente
15
= 3600, con lo que N = 23 procesadores. En la práctica el número de
expresión: N 2×10
×25×109
procesadores necesarios probablemente será mayor, dado que estamos despreciando la sobrecarga
introducida por la parelización y suponemos que la aplicación escala perfectamente con el número
de procesadores y todo el tiempo de ejecución de la aplicación está dedicado a cálculos en punto
flotante que se reparten de manera perfecta entre todos los procesadores.
b) La ley de Amdahl nos dice que tenemos limitada la escalabilidad, y que este lı́mite depende de
la fracción de código no paralelizable. Más concretamente, en el razonamiento de Amdahl se
supone constante el tiempo de ejecución de una aplicación en un sistema uniprocesador y con
ello se considera también constante la fracción de código paralelizable. Sin embargo, en muchas
ocasiones se puede incrementar la fracción de código paralelizable aumentando el tamaño del
problema que resuelve la aplicación.
c) Desde el punto de vista del rendimiento ambas implementaciones difieren en el rendimiento
que obtienen las escrituras, más concretamente en la latencia de las mismas. En el primer
caso, la latencia de las escrituras permanece casi constante conforme aumentamos el número de
procesadores (y por tanto, compartidores potenciales). Por el contrario, en el segundo esquema
la latencia de las escrituras se incrementa con el número de compartidores. De esta forma,
diremos que el primer esquema es más escalable.
d) La compartición falsa surge, por ejemplo, cuando dos procesos están accediendo a partes distintas
de un mismo bloque de datos y al menos uno de ellos está escribiendo. El programador podrı́a
eliminar este fenómeno evitando que variables que no se comparten entre dos procesadores
caigan en la misma lı́nea de memoria. Para ello se podrı́an declarar variables “vacı́as” (padding)
entre las declaraciones de las dos variables no compartidas.
e) La transición M −
→ I serı́a preferible para bloques de datos que contienen variables que están
siendo accedidas según un patrón migrario por varios procesadores. Como ejemplo, la variable
global->diff en la práctica 3 se accede según ese patrón migratorio (primero se lee y después se
modifica su contenido).
f) Inicialmente, el vector de estados para el bloque de datos será V(c1, c2, c3, c4) = (I, I, I, I). Las
transacciones de bus que se generarán son:
Lect(core 1) : BusRd. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (S, I, I, I). El bloque de datos es proporcionado
por la memoria principal y cargado también el la L2 compartida.
5
Escr(core 1) : BusRdX. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (M, I, I, I). El bloque de datos es proporcionado
por la L2.
Lect(core 2) : BusRd. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (O, S, I, I). El bloque de datos es proporcionado
por la L1 del core 1.
Lect(core 3) : BusRd. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (O, S, S, I). El bloque de datos es proporcionado
por la L1 del core 1.
Escr(core 4) : BusRdX. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (I, I, I, M). El bloque de datos es proporcionado
por la L1 del core 1.
Lect(core 1) : BusRd. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (S, I, I, O). El bloque de datos es proporcionado
por la L1 del core 4.
reemplazo(core 4) : BusWB. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (S, I, I, I). El bloque de datos es actualizado en
la L2 compartida.
Lect(core 2) : BusRd. Una vez completada la transacción de bus, el vector de estados
quedarı́a de la forma V(c1, c2, c3, c4) = (S, S, I, I). El bloque de datos es proporcionado
por la L2 compartida.
g) El modelo de consistencia débil permite cualquier reordenación entre escrituras y lecturas a
posiciones de memoria distintas por parte del mismo procesador (relaja W−
→R, W−
→W, R−
→R,
R−
→W), manteniendo el orden entre accesos sólo en los puntos de sincronización del código. Para
el ejemplo que se nos da, el resultado de la ejecución bajo ambos modelos serı́a la impresión de
un valor 1 por pantalla.
h) La principal diferencia es que la versión basada en tickets es justa, es decir, otorga el cerrojo
en el mismo orden en el que los procesos van tratando de adquirirlo, mientras que la versión
basada en LL-SC no.
i) El problema de la versión original es que se puede producir un interbloqueo si se hace uso de
la misma variable barrera en dos puntos del programa. En concreto cuando se libera la barrera
en el primer punto, todos los procesos deben salir antes de que un proceso vuelva a entrar en
la misma variable barrera en el segundo punto. Como ejemplo, podrı́amos tener que uno de los
procesos que alcanzó la barrera en el primer punto permanece expulsado de la CPU cuando el
último proceso en llegar a la barrera la libera. Si uno de los procesos alcanza el segundo punto
del programa antes de que el proceso anterior haya visto la liberación de la barrera se produce
el interbloqueo. Para evitar este problema, la versión con cambio de sentido hace que la espera
ocupada se haga sobre valores distintos en llamadas consecutivas a la misma variable barrera,
de forma que no hay que inicializar la variable que empleamos para hacer la espera ocupada.
j) Ambas técnicas de conmutación segmentan el envı́o de los mensajes, la diferencia es que la
primera realiza el control de flujo sobre paquetes completos, lo que hace que los buffers del
router tengan capacidad para almacenar al menos todo un paquete, mientras que la segunda
ejerce el control de flujo a nivel de flit (unidades más pequeñas) de forma que el tamaño de los
buffers puede ser menor.
k) El ejemplo más claro es el encaminamiento en orden de dimensión (dimension order ). En este
caso se recorren las dimensiones en un orden fijo, agotando los desplazamientos en cada una de
ellas antes de pasar a la siguiente.
¤
6
4.
(3 puntos) Dado el siguiente programa secuencial escrito en C:
#include ...
if (max == 0) break;
key index[pos] = 0;
value = key[max];
for (i=0;i<8000;i++) {
for (j=0;j<8000;j++) {
A[i][j] = A[i][j] * value;
}
}
float key[256];
unsigned char key index[8000]; /* valores entre 0 y 255 */
float A[8000][8000];
void initialize(void) {
/* Inicializa los arrays key y
key index, y la matriz A */
}
}
void solve(void) {
int max;
int i,j,pos;
float value;
int main() {
initialize();
solve();
return 0;
}
while (1) {
max = 0;
for (i=0;i<8000;i++) {
if (key index[i]>max) {
max = key index[i];
pos = i;
}
}
muestra una versión paralela del código1 que utilice 4 hilos y que por tanto pueda ser ejecutado
en un CMP con 4 núcleos. Supón que nos interesa paralelizar los accesos tanto a la matriz A como a
key index. Para ello, discute las fases en las que habrı́a que organizar el código paralelo, las variables
que habrı́a que declarar como compartidas, etc.
Solución
El programa paralelo lo organizarı́amos en tres fases: (1) fase de inicialización, en la que entre
otras cosas se crearán los 3 hilos adicionales para el cálculo, (2) fase paralela, que consistirı́a en
la ejecución del procedimiento solve por parte de los cuatro hilos, y (3) fase de finalización en la
empleamos la llamada wait for end(). A su vez, el procedimiento solve se organizará en dos fases:
(1) cálculo del máximo elemento en la porción del array key index que cada hilo tiene asignada y
posteriormente cálculo del máximo global de entre los máximos encontrados por cada hilo y (2)
actualización de la matriz A.
Los arrays key index y key, y la matriz A habrı́a que declararlos como memoria compartida entre
todos los hilos. Además necesitamos un par de variables globales para almacenar el máximo global
(global max ) y su posición en el array key index (global pos). Finalmente necesitamos también una
variable global pid que vamos a utilizar para obtener el identificador único que cada hilo va a tener
asignado. En cuanto a las primitivas de sincronización vamos a hacer uso de un cerrojo (l1 ) y una
barrera (b1 ).
El código pedido quedarı́a como sigue:
¤
1
Utiliza las macros parmacs vistas en clase:
lockdec(lock), lock(lock) y unlock(lock)
bardec(bar) y barrier(bar,num threads)
g malloc(num bytes), create(num threads, proc, arguments) y wait for end(num threads)
7
#include ...
if (max != 0) {
LOCK(l1);
if (max > var->global max) {
var->global max = max;
var->global pos = pos;
}
UNLOCK(l1);
}
BARRIER(b1);
if (var->global max == 0) break;
if (pid == 0) key index[var->global pos] = 0;
value = key[var->global max];
for (i=0;i<2000;i++) {
for (j=0;i<8000;j++) {
A[min i+i][j] = A[min i+i][j] * value;
}
}
BARRIER(b1);
float *key;
unsigned char *key index;
float **A;
struct {
int global pid;
int global max;
int global pos;
} *var;
LOCKDEC(l1);
BARDEC(b1);
void initialize(void) {
/* Inicializa los arrays key y
key index, y la matriz A */
}
void solve(void) {
int max,pid;
int i,j,pos;
float value;
int min i;
LOCK(l1);
pid = var->global pid;
var->global pid++;
UNLOCK(l1);
min i = pid*2000;
while (1) {
max = 0;
if (pid==0) var->global max = 0;
for (i=0;i<2000;i++) {
if (key index[min i+i]>max) {
max = key index[min i+i];
pos = min i+i;
}
}
}
}
int main() {
key = G MALLOC(array 1-d de 256 elementos float);
key index = G MALLOC(array 1-d de 8000 elementos char);
A = G MALLOC(array 2-d de 8000 por 8000 elementos float);
var = G MALLOC(struct con 3 elementos enteros);
initialize();
CREATE(3, solve);
solve();
WAIT FOR END(3);
return 0;
}
8
Descargar