Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

ASD - Arquitectura de Sistemas Distribuidos PDF

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 322

Escuela Técnica Superior de Ingeniería Informática

Arquitectura de
Sistemas Distribuidos
Departamento de Arquitectura y Tecnología de Computadores

Miguel Angel Cifredo Campos TERCERO


Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1 Conceptos Básicos de Sistemas Distribuidos y Paralelos.

1.1 Arquitectura: Visiones clásica y actual.

 Visión “clásica”: Arquitecturas basadas en el juego de instrucciones [ISA, Instruction Set Architecture]:

- CISC (Complex Instruction Set Computer) Computador con un conjunto de instrucciones complejas.

- RISC (Reduced Instruction Set Computer) Computador con un conjunto reducido de instrucciones,
posteriormente llamado de instrucciones sencillas.

Se pasó de la arquitectura CISC a RISC debido a la aparición de compiladores externos, aumentándose el


rendimiento de un 25% al año a un 52% al año. Además, la paralelización pudo surgir debido a la
aparición de diferentes tipos de instrucciones: de gestión de datos y operacional aritmético.

 Visión “actual”: En función de los requisitos de la máquina de destino (target machine).

A partir de aquí construimos una arquitectura dependiendo del problema y de la máquina de destino.
Aparece la GPU (arquitectura creada para el tratamiento de vídeos, fotos…) optimizada con librerías, de
forma que el procesador se libera de realizar dichas tareas, por tanto, se maximiza el rendimiento pero
a base de restricciones de coste, potencia, disponibilidad, herramientas software, etc.

A debate:

- ¿Cuál de las 2 opciones es más correcta?

- ¿Interesa dominar todos los tipos de arquitectura? O por el contrario ¿interesa concentrarse en la
arquitectura actual de más prestaciones o rendimiento?

1
1.2 Leyes y principios de Arquitectura.

1.2.1 Ley de Moore.

La Ley de Moore expresa que «aproximadamente cada dos años se duplica el número de
transistores en un circuito integrado». Esto es, entre un 40% y un 55% cada año; luego se
cuadriplica el número de transistores de un chip cada tres años. Este incremento está
apoyado en las evoluciones de las arquitecturas. Además, se pueden añadir más niveles de
caché, mayores algoritmos, predictores de salto…
Gordon Moore

Se hace una extensión a toda la tecnología:


22%/año desde 2005 consecuencias de comparar uniprocesadores
frente a memoria respecto a cachés más eficientes.
52%/año desde 1986
Por otra parte, la memoria no ha crecido en
prestaciones, ni tampoco en capacidad, como lo ha
25%/año antes de 1986
hecho el procesador. Cuando aparecieron los
multiprocesadores, las prestaciones aumentaron
7% / año pero la frecuencia bajó, razón por la que la
pendiente de la gráfica desciende un 22%.

Sabiendo que:

A1 = A0 ∙ r
A2 = A1 ∙ r y=a+xb
...
An = A0 ∙ rn

log(𝐴𝑛 ) = log(𝐴𝑜 ) + log(𝑛𝑛 ) = ⏟


⏟ log(𝐴𝑜 ) + ⏟
n ∙ log(𝑟)
𝑦 𝑎 𝑥∙𝑏

La importancia de la jerarquía de memoria ha aumentado con los avances en el rendimiento de los procesadores.
En la figura anterior, se muestra el rendimiento del procesador respecto a la mejora histórica en cuanto al tiempo
de acceso a la memoria principal. La línea de los procesadores muestra el aumento de las solicitudes promedio
de memoria por segundo (es decir, la inversa de la latencia entre referencias de memoria), mientras que la línea
de la memoria muestra el aumento de accesos en la DRAM por segundo (es decir, la inversa de la latencia de
acceso DRAM). La situación en los monoprocesadores es realmente un poco peor, ya que la velocidad pico de
acceso a memoria es más rápido que la tasa media, que es lo que se representa.

A partir de 1980 el rendimiento es la línea de base, la diferencia en el rendimiento, medido como la diferencia
en el tiempo entre las peticiones de memoria del procesador (para un solo procesador o núcleo) y la latencia de
un acceso DRAM, se representan gráficamente en el tiempo. Hay que tener en cuenta que el eje vertical debe
ser en una escala logarítmica para representar el tamaño de la diferencia de rendimiento procesador - DRAM. La
línea de base es de 64 KB de memoria DRAM en 1980, con una mejora del rendimiento de 1’07 por año en la
latencia. La línea de procesadores supone una mejora del 1’25 por año hasta 1986, una mejora de 1’52 hasta el
año 2000, una mejora de 1’20 entre 2000 y 2005, y ya, ningún cambio en el rendimiento del procesador (en
función de cada núcleo), entre 2005 y 2010.

2
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Tendencias tecnológicas: coste y capacidad (2007-2012):


Densidad del transistor: 35% / año
Tecnología de circuitos integrados { Tiempo de vida: 10% - 20% / año
Integración global: 40% - 55% / año

Capacidad Memoria DRAM: 25% - 40% / año (lentamente)

Capacidad Memoria Flash: 50% - 60% / año (15x-20x más barato por bit que la memoria DRAM)

15x-25x más barato por bit que la memoria Flash


Tecnología de disco magnético: 40% / año {
300x-500x más barato por bit que la memoria DRAM

Coste y capacidad de la memoria DRAM: Ancho de banda y Latencia:

Factores a tener en cuenta:

- Incremento de la capacidad 4x.


- Incremento del coste inicial 2x aprox.
- Nueva revisión cada 2 años.
- Puesto en coste 2$ aprox.
- Incremento de la velocidad 2x aprox.

Rendimiento del monoprocesador:

Multiprocesadores

CISC
RISC

3
1.2.2 Ley de Amdahl

Amdahl pretende averiguar la mejora máxima global de un sistema cuando sólo una parte
de éste es mejorado. La Ley de Amdahl establece que:

« La mejora obtenida en el rendimiento de un sistema, debido a la alteración de uno de


sus componentes, está limitada por la fracción de tiempo que utiliza dicho componente »

Sin embargo, optimizar un porcentaje de instrucciones de un código, no supone obtener


una aceleración en el mismo porcentaje. Por tanto, debemos optimizar (hacer rápido o
más simple) el caso común o fragmentado del código que más se repite, los bucles.
Eugene Myron Amdahl

Asumiendo que una tarea tiene dos partes independientes,


A y B, consumiendo B el 25% del tiempo total de
computación. Optimizando mucho se puede realizar la parte
B 5 veces más rápido y sin embargo, sólo reduce un poco el
tiempo de computación; en contraste, una pequeña mejora
de la parte A hace que ésta vaya el doble de rápido. Por tanto
es mucho mejor la optimización de la parte A que la de la
parte B aunque se mejore mucho más B (5x contra 2x).

El incremento de la velocidad de un programa utilizando


múltiples procesadores en computación distribuida está
limitado por la parte secuencial del programa. Por ejemplo,
si media parte del programa (0’50%) es computación
secuencial, el incremento de velocidad máximo teórico
(aceleración) con computación distribuida será de:
1
Aceleración =
0' 50
(1 - 0'50) +
Número de procesadores

T ejecución inicial

1-F F

1-F F/Aceparcial
T ejecución final
Normalizamos:
Tejecución inicial = 1, donde 0 ≤ F ≤ 1,
Tejecución inicial 1
F Aceleraciónparcial = =
Tejecución final F
Tejecución final = (1 - F) + (1 - F) +
Aceleraciónparcial } Aceleraciónparcial
4
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Llegamos a la conclusión de que no se puede alcanzar el paralelismo ideal. Tan sólo una porción σ del programa
puede ser ejecutada en paralelo.

1 1
Si p>> entonces Speedup → Valor de la asíntota = (1 - σ) +
σ = 1−σ
= (1 − σ)−1 .

Ejercicio: Supongamos que consigo paralelizar el 50% de un programa, de forma que se puede ejecutar en 10
procesadores a la vez (obteniendo una aceleración parcial de 10). ¿Cuál sería la aceleración total?
1
Aceleración = = 1'81̂
0'50
(1 - 0'50) +
10
El resultado indica que la máquina paralela es un 81% más rápida que la máquina secuencial.

Nota: La aceleración debe ser siempre mayor o igual a 1. Si fuera menor, significaría desaceleración.

Ejercicio: Suponer que estamos considerando una mejora que corra 8 veces más rápida que la máquina original,
pero sólo es utilizable el 40% del tiempo. ¿Cuál es la aceleración global lograda al incorporar la mejora?

Fracción mejorada = 0’40

Aceleración mejorada = 8
1
Aceleración global = 0'40 = 1'53
(1 - 0'40) +
8

La Ley de Amdahl da lugar a la Ley de Rendimientos Decrecientes: «La mejora incremental en la aceleración
conseguida por una mejora adicional en el rendimiento de una parte del cálculo disminuye tal como se van
añadiendo mejoras». Un corolario importante de la Ley de Amdahl es que si una mejora sólo es utilizable por
una fracción de una tarea, no podemos aumentar la velocidad de la tarea más que el recíproco de 1 menos esa
fracción.

Un error común al aplicar la Ley de Amdahl es confundir “fracción de tiempo convertido para utilizar una mejora”
y “fracción de tiempo después de utilizar la mejora”. Si, en lugar de medir el tiempo que podría utilizar la mejora
en un cálculo, midiésemos el tiempo después de utilizar la mejora, los resultados serían incorrectos.

La Ley de Amdahl puede servir como guía para determinar cuánto una mejora aumenta el rendimiento y cómo
distribuir los recursos para mejorar la relación costo/rendimiento. El objetivo, claramente, es emplear recursos
de forma proporcional al tiempo que se requiere en cada parte.

5
Entonces, ¿cómo podemos mejorar la aceleración?

 Aumentando el número de procesadores.


 Optimizando el código de la aplicación.

Aumentar indiscriminadamente el número de microprocesadores es una opción insostenible. Por lo tanto,


tendremos que decantarnos por optimizar el código más común de los programas, el de los bucles.

Supongamos que queremos obtener una aceleración de valor 10 utilizando 80 máquinas. ¿Qué porcentaje del
código tendremos que optimizar?
1
Atotal = Fragmento = 10
(1 - Fragmento) +
80
Fragmento 10 Fragmento
1 = 10 ∙ ((1 - Fragmento) + ) ⇒ 10 - 10 Fragmento + = 1 ⇒ Fragmento = 0'91
80 80

Por tanto, tenemos que paralelizar el 91 % del código para aumentar la aceleración un valor de 10.

1.2.3 Principio de Localidad (10/90).

Se trata de una regla empírica que indica que « un procesador emplea el 90% del tiempo en la ejecución del 10%
del código ». Es importante así, el orden de los accesos a memoria. Por tanto, si conseguimos que ese 10% de
tiempo esté lo más cerca posible del procesador, aumentamos en un 90% la optimización. Esta regla se puede
plantear como el Principio de Localidad (todos los programas favorecen una parte de su espacio de direcciones
en cualquier instante de tiempo). Tiene dos dimensiones: Localidad temporal (si se referencia a un elemento,
tenderá a ser referenciado pronto) y Localidad espacial (Si se referencia a un elemento, los elementos cercanos
a él tenderán a ser referenciados).

De esta forma, aparecen las cachés de distintos niveles por lo que el procesador tiene poco tiempo de espera.

Ejercicio. Teniendo en cuenta la Ley de Amdahl y la del Principio de Localidad 10/90, si se quiere optimizar un
código, conviene más concentrarse en los bucles que realicen muchas iteraciones.

Por ejemplo, sean los códigos:

for ( i = 0; i < TAM ; i++){ for ( i = 0; i < TAM ; i++){


for (j = 0; j < TAM ; j++){ for (j = 0; i < TAM ; i++){
a[i][j] = b[i][j]; a[i][j] = b[j][i];
} }
} }

MR = 1/4 MR = 1

Como el tamaño de la memoria caché suele ser menor que el tamaño del vector, no se puede guardar en ella tal
cantidad de datos.

6
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio:

 Section A: 180 líneas, que es el 90% del código pero supone el 10% del tiempo de ejecución.
 Section B: 20 líneas, que es el 10% del código pero supone el 90% del tiempo de ejecución.

Mejora de 90 en A:
T 1
→ = 1'11
1 1
( )
90 TA + TB 90 ∙ 0'10 + 0'90

Mejora de 10 en B:
T 1
1 → 1 = 5'26
TA + 10 ∙TB 0'10 + (10) ∙ 0'90

7
1.3 Rendimiento y productividad.

Son las componentes del tiempo de ejecución de un programa.

 Productividad (throughput): Número de tareas que puede realizar una máquina en un segundo. Duplicar
el número de máquinas implica duplicar la rentabilidad, pero cada tarea sigue tardando el mismo tiempo.

 Rendimiento (performance): Inversa del tiempo que se tarda en realizar una tarea. No es un factor fácil
de aumentar.
1
Rendimiento =
Tiempoejecución tarea

 El aumento del rendimiento implica el aumento de la productividad.

 El aumento de la productividad no es consecuencia implícita del aumento del rendimiento.

1.3.1 Componentes del tiempo de ejecución de un programa.

𝐶𝑃𝐼𝑏𝑙𝑜𝑞𝑢𝑒𝑜 = 𝐶𝑃𝐼𝑟𝑒𝑎𝑙 − 𝐶𝑃𝐼𝑖𝑑𝑒𝑎𝑙

𝐶𝑃𝑈𝑡𝑖𝑒𝑚𝑝𝑜 = 𝑇𝑖𝑒𝑚𝑝𝑜𝑒𝑗𝑒𝑐𝑢𝑐𝑖𝑜𝑛 = 𝐶𝑃𝑈𝑐𝑖𝑐𝑙𝑜𝑠 ∙ 𝑃𝑒𝑟𝑖𝑜𝑑𝑜_𝑑𝑒𝑙_𝑐𝑖𝑐𝑙𝑜_𝑑𝑒_𝑟𝑒𝑙𝑜𝑗

𝐶𝑃𝑈𝑐𝑖𝑐𝑙𝑜𝑠
𝐶𝑃𝑈𝑡𝑖𝑒𝑚𝑝𝑜 =
𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎 𝑑𝑒𝑙 𝑟𝑒𝑙𝑜𝑗

𝐶𝑃𝑈𝑐𝑖𝑐𝑙𝑜𝑠
𝐶𝑃𝐼 =
𝑁º 𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠

𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐. 𝑐𝑖𝑐𝑙𝑜𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜𝑠


𝑇𝑒𝑗𝑒𝑐𝑢𝑐𝑖𝑜𝑛 = 𝑁º 𝑖𝑛𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 ∙ 𝐶𝑃𝐼 ∙ 𝑃𝑒𝑟𝑖𝑜𝑑𝑜_𝑑𝑒𝑙_𝑐𝑖𝑐𝑙𝑜_𝑑𝑒𝑙_𝑟𝑒𝑙𝑜𝑗 ≡ ∙ ∙ →
𝑝𝑟𝑜𝑔𝑟𝑎𝑚𝑎 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐. 𝑐𝑖𝑐𝑙𝑜𝑠 𝑝𝑟𝑜𝑔𝑟𝑎𝑚𝑎

1
𝑅𝑒𝑛𝑑𝑖𝑚𝑖𝑒𝑛𝑡𝑜 =
𝑡𝑒𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛

1
𝑇𝑒𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛,𝑎𝑛𝑡𝑒𝑠 𝑅𝑒𝑛𝑑𝑖𝑚𝑖𝑒𝑛𝑡𝑜2 𝑎 ≥ 1. 𝑥
𝑅𝑒𝑛𝑑𝑖𝑚𝑖𝑒𝑛𝑡𝑜1
𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛 = = = ⇒ [
𝑇𝑒𝑗𝑒𝑐𝑢𝑐𝑖𝑜𝑛,𝑑𝑒𝑠𝑝𝑢é𝑠 1 𝑅𝑒𝑛𝑑𝑖𝑚𝑖𝑒𝑛𝑡𝑜1 𝑥% 𝑚á𝑠 𝑟á𝑝𝑖𝑑𝑎 𝑙𝑎 𝑚á𝑞𝑢𝑖𝑛𝑎 𝑑𝑒𝑙 𝑐𝑜𝑐𝑖𝑒𝑛𝑡𝑒
𝑅𝑒𝑛𝑑𝑖𝑚𝑖𝑒𝑛𝑡𝑜2

8
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.3.2 CPI. Magnitud arquitectónica.

El análisis del CPI de un programa es una tarea complicada.

El CPIReal está formado por : CPIIdeal + CPI


⏟ Blq.Datos + CPIBlq.Estructural + CPIBlq.Control + CPIBlq.Memoria
CPIBloqueo

Actualmente casi se consigue el límite del flujo de datos (Data Flow Limit). Los dos factores que más nos limitan
son: la predicción de saltos y los accesos a la memoria caché de datos.

 Los CPIBloqueo son tiempos en los que el procesador no realiza nada. Sólo “se ven” las dependencias reales a
los largo del tiempo (pipeline) de mayor transcendencia, las más simples se “enmascaran”. Veamos un
ejemplo de enmascaramiento:

# Fibonacci # Fibonacci Flujo de datos


# Código normal # Desenrrollado del bucle intrínseco al
x[0] = 0; x[1] = 1; x[0] = 0; x[1] = 1;
propio algoritmo
for ( i = 2; i < N ; i++){ for ( i = 2; i < N ; i += 3){
x[i] = x[i-1] + x[i-2]; x[i] = x[i-1] + x[i-2];
} x[i+1] = x[i-0] + x[i-1];
x[i+2] = x[i+1] + x[i+2];
}

 El CPIEstructural es el menos común, generalmente porque hay pocas ALUs (UF Unidades Funcionales).

# Fibonacci
c = 0; b = 1;
for ( i = 2; i < N ; i++){
a = b + c;
c = b; Son operadores aritméticos de tipo entero (ADD, MOVE, MOVE)
b = a;
}

 El CPIControl hace referencia a la gestión del PC (Program Control) con las instrucciones JUMP y BRANCH. Hoy en
día existen predictores de salto de forma que, si acierta entonces no hay bloqueo. Este acierto suele
producirse en bucles grandes, donde siempre se acierta salvo en la última iteración.

 El CPIMemoria hace referencia a los fallos de caché (L1, L2, L3, RAM, HD,…) debido a que los datos grandes no
caben en la memoria caché de nivel 1 (L1). Se trata de reducir los fallos de memoria caché L1.

Veamos un ejemplo para recorrer una matriz, que resolvemos de 2 formas. Debemos tener presente que los
datos estructurados se almacenan en memoria en forma de lista continua, esto es, que nuestra
representación bidimensional está realmente almacenada como una única sucesión de sus distintas filas. Por
lo que si recorremos cada uno de los vectores aprovechamos el principio de localidad.
# 1ª Forma # 2ª Forma
for ( f = 0; f < N ; f++){ for ( c = 0; c < N ; c++){
for ( c = 0; c < N ; c++){ for ( f = 0; f < N ; f++){
a[f][c] = 0; a[c][f] = 0;
} }
# Mejor solución (90% de ejecución)

9
 ¿Qué factores limitan más a una máquina secuencial?

 CPI Blq.Control Mediante la predicción de saltos.


 CPI Blq.Memoria Cuando ya no se encuentra en la memoria de datos aquellos a los que debe
acceder haciendo uso entonces de las memorias cachés.

 ¿Qué factores limitan más a una máquina encadenada?

 CPI Blq.Datos Datos que se generan más tarde.


 CPI Blq.Estructural Dos instrucciones comparten un mismo recurso.
 CPI Blq.Control Saltos tomados o no tomados.

 ¿Qué factores limitan más el rendimiento?

 CPI Blq.Control Si no tenemos un buen predictor de salto, se desaprovecha la ejecución de


muchas instrucciones mientras se está calculando si el salto será tomado o no.
 CPI Blq.Memoria Gracias al Principio de Localidad (10/90), el CPI Blq.Memoria no es tan importante,
pues los datos suelen estar bien ubicados.

Con un buen predictor se puede analizar un código el código de forma general, si en nuestro caso se descubre
que se toma un 70% de los saltos y en el otro 30% restante no se toma, es lógico, pues en un bucle se toman
todos los saltos excepto el último. Si el salto es tomado, lo será casi siempre y viceversa. Así, por la Ley de Amdahl,
debemos optimizar el caso común, el 70%.

Para conseguir el rendimiento óptimo de una instrucción por ciclo, otro de los obstáculos que nos encontramos
es el de las dependencias de control, esencialmente, los saltos. Para lograrlo contamos con elementos que nos
permite la gestión de los saltos tomados y no tomados, esto son:

- BPB (Buffer Predictor Branch, Buffer de predicción de saltos): Cada salto va a llevar un historial de
comportamientos mediante una máquina de estados.

- BTB (Buffer Target Branch, Buffer de destinos de saltos): Se le añade la dirección de destino a la que va
a ser tomado. El porcentaje de acierto del BTB es de un 97%, por lo que estamos ahorrando muchos
ciclos de bloqueo.

BPB BTB
10
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.4 Magnitudes de rendimiento.

Hay magnitudes muy usadas pero no son exactas e independientes del software y del sistema operativo, como:

o MIPS: Millones de instrucciones por segundo. Esta magnitud se propuso cuando las instrucciones eran
sencillas (instrucciones enteras). Por ejemplo, para el cálculo de una función trigonométrica (ej. Sen(α)),
se traducía esta función compleja en otras funciones más sencillas. Cuando llegaron los procesadores
capaces de ejecutar las instrucciones complejas, hacían lo mismo pero con menos instrucciones en
ensamblador. Por tanto, su MIPS es menor (tiene menos instrucciones) pero se ejecuta más rápido.
𝑁º 𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 𝑁º 𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 𝑓𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎
𝑀𝐼𝑃𝑆 = = 6 =
10 ∙ 𝑇𝑖𝑒𝑚𝑝𝑜𝐸𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛 10 ∙ (𝑁º 𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 ∙ 𝐶𝑃𝐼 ∙ 𝑃𝑒𝑟𝑖𝑜𝑑𝑜 )
6 106 ∙ 𝐶𝑃𝐼

Nota: Desaparece el número de instrucciones.

o MFLOPS (GFLOPS) Millones (Gigas) de operaciones en coma flotante por segundos (Flotating point
Operations Per Second). Se usan en código científico y son sólo con los operadores + - * / y otras
transcendentes. El problema que tienen es que dependen del juego de instrucciones que se estén
ejecutando en el código: las instrucciones enteras tardan menos tiempo de ejecución que las
instrucciones en punto flotante.
𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑜𝑝𝑒𝑟𝑎𝑐𝑖𝑜𝑛𝑒𝑠 𝑒𝑛 𝑐𝑜𝑚𝑎 𝑓𝑙𝑜𝑡𝑎𝑛𝑡𝑒 𝑟𝑒𝑎𝑙𝑖𝑧𝑎𝑑𝑎𝑠
𝑀𝐹𝐿𝑂𝑃𝑆 =
106 ∙ 𝑇𝑖𝑒𝑚𝑝𝑜𝐸𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛 (𝑠)

Por otro lado, hay mejores medidas como los valores medios geométricos de un conjunto determinado de
programas (colección benchmarks).

o SPEC, Standard Performance Evaluation Corporation. Son de dos tipos:


- SPECint Programas con operaciones únicamente enteras.
- SPECfp Programas con operaciones únicamente de coma flotante.

𝑁
𝑛
𝑀𝑒𝑑𝑖𝑎 𝑔𝑒𝑜𝑚é𝑡𝑟𝑖𝑐𝑎: 𝐸̅ = √∏ 𝑡𝑖
𝑖=1

𝑡𝑦
Tienen la siguiente propiedad: 𝐴𝑦𝑥 = 𝑡 Si lo dividimos por el tiempo de referencia de una tercera
𝑥
máquina (histórica o bien tabulada por el SPEC), entonces con el valor obtenido se puede calcular la
aceleración de la nueva con respecto a la de referencia. Por tanto,

𝑡𝑦1
𝑁
𝑛 ⁄𝑡
̅̅̅̅̅ 𝑖
𝐴𝑦𝑥 = √∏ 𝑡
𝑥1
𝑖=1 ⁄𝑡
𝑖

o TPC, Transaction Processing Council. Trata las entradas y salidas (Input/Output).

o EMBC, Embedded Microprocessor Benchmark Consortium. Para sistemas empotrados.

Estas medidas de rendimiento se utilizan dentro de un benchmark concreto, en el que se conocen todas las
opciones de compilación, ISA, … y tomando como referencia una máquina de la era actual a la que evaluamos.
Sin embargo, el problema que se le puede encontrar es que hay máquinas que tienen implementado benchmarks
en hardware, por lo que se ejecuta más rápido en hardware.
11
1.5 Tipos de paralelismo.

Podemos establecer cuatro tipos diferentes de paralelismo:

 Paralelismo a nivel de instrucciones (ILP, Instruction Level Parallelism).


 Paralelismo a nivel de datos (DLP, Data Level Parallelism).
 Paralelismo a nivel de hilos, tareas (TLP, Task/Threads Level Parallelism).
 Paralelismo a nivel de peticiones (RLP, Request Level Parallelism).

Actualmente el paralelismo es casi la única forma de aumentar el rendimiento y/o la productividad. El proceso
de implementación del paralelismo implica procesar una instrucción que incluye un número de operaciones más
simples. Dichas operaciones se organizan en una secuencia de etapas.

12
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.5.1 Paralelismo a nivel de instrucciones (ILP)

Ejecución de un procesador secuencial:

Instr. 1 → Instr. 2 → Instr. 3 → Instr. 4

Ejecución encadenada de instrucciones de código máquina (segmentación o pipelining) sin replicación de


recursos:

Inst. 1 Fetch Dec Exec WB


Inst. 2 Fetch Dec Exec WB
Inst. 3 Fetch Dec Exec WB
Inst. 4 Fetch Dec Exec WB
4 Instrucciones en el aire

Se incrementa la productividad (throughput); esto es, el número de instrucciones ejecutadas por segundo.

Se incrementa la latencia; esto es, el tiempo requerido para ejecutar una instrucción individualmente.

Se producen retrasos adicionales debido a: Registros de la cadena y División no uniforme de las etapas.

Se combina el encadenamiento con la replicación de recursos.

Tendremos 3 x 4 instrucciones en el aire ( p = 4 : número de etapas )

13
Hoy en día estamos cercanos al límite del flujo de datos (Data Flow Limit).

¿Qué dos factores limitan más el paralelismo ILP extraíble en benchmarks reales?

– Fallos en la predicción de saltos: La BTB es cada vez más sofisticada, compleja y grande.

– Accesos a memoria caché de datos: Aunque tiene un ancho de banda (AB MEM) altísimo, la latencia de
acceso decrece poco, y tiene muchos accesos.

Se plantean dos enfoques de planificación con el objetivo de reducir el tiempo de ejecución de un programa en
una máquina encadenada:

- Planificación Dinámica: (Hardware) La realiza el procesador en tiempo de ejecución. Se trata de un


algoritmo ya implementado en hardware, lo que resulta muy complejo. La idea es escribir la información
de las dependencias en unos registros ocultos y esperar a que llegue el dato, es lo que denominamos
ejecución fuera de orden. Usado en la mayoría de servidores y procesadores de escritorio. Todavía no se
ha extendido completamente a los procesadores usados en dispositivos móviles (ARM).

- Planificación Estática: (Software) Reordenación de las instrucciones del programa (Scheduling).


Destinado a compiladores avanzados por lo que no es muy usado fuera de las aplicaciones científicas.
En teoría se puede obtener un alto rendimiento del Scheduling Estático con el desenrollado de bucles.

Ejemplo de desenrollado de bucles: // Bucle sin desenrollar: // Bucle desenrollado (4 it.)


for (i = 0; i < M; i++) for (i = 0; i < M; i += 4) {
y[i] = x[i] * s; y[i+0] = x[i+0] * s;
y[i+1] = x[i+1] * s;
y[i+2] = x[i+2] * s;
y[i+3] = x[i+3] * s;
}

Arquitecturas de microprocesadores (Tres Modelos):

Para reducir el tiempo de ejecución en una máquina encadenada deberemos reducir uno de los operandos de la
siguiente ecuación: CPU = NºInstrucciones x Ciclos_por_instrucción x Ciclo_reloj.

o Reducir el número de instrucciones. Procesadores LIW o VLIW (Very Long Instruction Word). Se trata de
macroinstrucciones con un paralelismo explícito, esto es, preparadas contener varias operaciones dentro
de un bloque. Utiliza técnicas de planificación estáticas, lo que supone gran dependencia del compilador.

o Reducir los ciclos por instrucción (CPI de los superescalares). Ejecutan varias
instrucciones por ciclo. Utilizan técnicas dinámicas, lo que supone una mayor
complejidad del hardware.

o Reducir т (ciclo del reloj). Procesadores superpipeline, supersegmentado o superencadenado. Por tanto,
la variación de la velocidad del reloj hará que tenga más etapas en la cadena y mayor consumo de energía
o potencia.

Ejemplo: 8 instrucciones en vuelo

14
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Microprocesadores actuales de alto rendimiento:

 Procesadores GPP (General Purpose Processor), muy sofisticados:


- POWER7 (IBM, Freescale y otros).
- Core (Intel).
- Sempron, Phenom, Fusion (AMD).

 Procesadores para sistemas empotrados: VLIW o superescalares sencillos para DSP, PDAs, smartphones…
- ARM v7 Cortex.
- OMAP3 processors (Texas Instrument).
- MSC81xx (Freescale) multi-core DSP.

Evolución de una familia de microprocesadores:

Esta tabla muestra las características de cuatro procesadores IBM Power. Todos salvo el Power6 tuvieron
planificación dinámica, el cual la tuvo estática, y por tanto, todos los predecesores mantuvieron dos pipelines de
carga y almacenamiento. El Power6 tiene las mismas unidades funcionales que el Power5 salvo por una unidad
decimal. El Power7 utiliza DRAM para el nivel L3 de caché.

15
1.5.2 Paralelismo a nivel de datos (DLP).

Se basa en el procesamiento vectorial de aplicaciones, donde se


ejecuta la misma operación sobre muchos datos; por ejemplo, sumar
dos vectores.

En la fase de ejecución se duplica la ALU y se ejecutan dos


operaciones en un mismo ciclo.

El uso más habitual lo encontramos en aplicaciones del siguiente tipo:

– Multimedia: Tratamiento de imágenes, sonido,…


– Científico: Cálculo matricial, vectorial, resolución de ecuaciones,…
– Transacciones: Bancos, comercios, comunicaciones,…

Uso en diferentes sistemas:

– Empotrados, llamados también genéricos.


– GPU (Graphics Processings Units): Tarjetas gráficas.
– Supercomputadores científicos.

Sus puntos débiles son:

– Es difícil de vectorizar.
– El juego de instrucciones cambia cada varios años, lo que conlleva al cambio de librerías y cambio en el
proceso de vectorización.

16
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.5.3 Paralelismo a nivel de tareas o hilos (tasks or threads) (TLP).

Se basa en el multiprocesamiento (multicore); esto es, en la ejecución paralela de distintos fragmentos de un


mismo programa sobre un conjunto de procesadores.

Los puntos débiles para conseguir el paralelismo son:

– Se trata de un proceso “artesanal” por parte de los programadores.


– El proceso se puede automatizar sólo si es evidente, es decir, sólo en casos triviales.
– Resulta difícil analizar si una aplicación es paralelizable, por lo que hay que confiar en las habilidades de
los programadores.

Los puntos clave en el diseño son:

– Contar con un subsistema (red) de interconexión.


– La forma de organizar la jerarquía de memoria.

Esquema de multiprocesador:

Según la disposición lógica de la memoria distinguimos:

 Espacio de direcciones compartido (Shared-Memory): Lo implementan todos los multicore actuales


(entre 2 y 8 núcleos), con una estructura rígida, es decir, con un único sistema operativo. A nivel de
programación se cuenta con herramientas como OpenMP, un conjunto de directivas o extensiones a
lenguajes de programación como Fortran, C, C++, etc.

 Espacio de direcciones disjuntos: Lo implementan multicomputadores o clústeres de computación que


trabajan a gran escala (>32 núcleos) débilmente acoplados, típico en la computación en la nube o en
aplicaciones científicas. A nivel de programación cuenta con la librería MPI (Message Passing Interface),
una especificación para de paso de mensajes.

17
1.5.4 Paralelismo a nivel de peticiones (RLP)

Suele darse en grandes servidores, con mucho paralelismo de datos donde se producen cientos de peticiones
independientes por segundo. Por lo que una misma petición externa suele dividirse en varias peticiones.

Cuello de botella
(tamaño de los buses)

Google Query-Serving Architecture

Paralelismo evidente en servidores:

- Carga de tareas variadas.


- Procesado de transacciones (base de datos, búsqueda internet, etc.).

Necesitan herramientas para:

- Balanceo de carga.
- Virtualización.

Aparecen nuevos retos:

- Alta disponibilidad.
- Reducción de costes.
- Reducción del consumo energético.

18
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.5.5 Avances actuales.

El cambio a varios procesadores por chip, alrededor de 2005, no vino de algún avance que simplificara
enormemente la programación paralela o del hecho de que fuera fácil el construir ordenadores multinúcleo. El
cambio se produjo porque no había otra opción debido a la imposibilidad de seguir incrementando el ILP.

El incremento en el rendimiento es ahora una tarea del programador. La era en la que los programadores
dependían de los diseñadores de hardware para acelerar sus programas, sin mover un dedo ha terminado.

Si los programadores quieren que sus programas vayan más rápidos en cada generación, tienen que hacer sus
programas más paralelos.

La versión popular de ley del incremento del rendimiento de Moore, en cada generación de la tecnología, ahora
depende de los programadores.

Organización del procesador Intel Core i7.

- Cuentan con 4 cores integrados en un único chip.

- Establecen una jerarquía de memoria para aprovechar


la localidad.

- Caches: Memoria de alta velocidad que contiene las


instrucciones y datos más recientemente accedidos.

Organización del procesador.

Control de Instrucciones:

- Leer una secuencia de instrucciones de la caché de


instrucciones (Fetch).
- Genera operaciones primitivas (Decode).

Unidad de ejecución:

- Unidades funcionales realizan las operaciones (Exec).


- Load/Store realiza los accesos a la caché de datos.
- Los resultados son almacenados en el fichero de
registros, obedeciendo a la semántica secuencial del
código máquina.

19
1.6 Data-Flow.

1.6.1 Data-Flow General.

Ejemplo:
for (i = 0 ; i < limit ; i++)
{
acc = acc * x[i];
}

Código máquina: i en %rdx, x[i] en %rax, limit en %rbp, acc en %xmm0


.L488:
LOAD %rax, x(%rdx*4) ; Carga x[i]
MUL %xmm0, %rax, %xmm0 ; Multiplica acc por x[i]
ADD $1, %rdx ; Incrementa el índice en i
CMP %rdx, %rbp ; Compara el límite i
JG .L488 ; Si >, ir al bucle

La figura superior es una representación gráfica del código del bucle interno. Las
instrucciones se mueven dinámicamente en una o dos instrucciones, cada una de
las cuales recibe los valores de las otras operaciones o de los registros y producen
valores para otras operaciones y registros. Se muestra el objetivo de la instrucción
final de la etiqueta loop, la cual salta a la primera instrucción mostrada.

La figura adjunta es una representación gráfica de computación Data-Flow con n


iteraciones realizadas por el bucle interno. La secuencia de las operaciones de
multiplicación define un camino crítico que limita el rendimiento del programa.

El programa tiene dos cadenas de dependencias de datos:

- La actualización de acc con la operación mul.


- La actualización de i con la operación add.

Dado que la multiplicación FP de simple precisión tiene una latencia (p.e. 4 ciclos)
mayor que la de la suma entera (p.e. 1 ciclo), la cadena de la izquierda tendrá un
camino crítico, que requiere 4∙n ciclos para ejecutarse.

La cadena de la derecha sólo requeriría n ciclos de ejecución, por lo tanto no limita


el rendimiento del programa.

20
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.6.2 Data-Flow. Desenrollado de bucles.

Ejemplo: El desenrollado (x2) reduce el efecto de instrucciones de overhead.


for (i = 0 ; i < limit ; i += 2)
acc = (acc*x[i]) * x[i+1];

Código máquina: i en %rdx, x[i] en %rax, limit en %rbp, acc en %xmm0

.L488:
LOAD %rax, x(%rdx*4) ; Carga x[i]
MUL %xmm0, %rax, %xmm0 ; Multiplica acc por x[i]
LOAD %rax, x((%rdx+1)*4) ; Carga x[i+1]
MUL %xmm0, %rax, %xmm0 ; Multiplica acc por x[i+1]
ADD $2, %rdx ; Incrementa el índice en i
CMP %rdx, %rbp ; Compara el límite i
JG .L488 ; Si >, ir al bucle

La figura superior es una representación gráfica del código del bucle interno. Cada
iteración tiene dos instrucciones mul, cada una de las cuales son enviadas y cargadas (load)
en una operación mul.

La figura adjunta es una representación de la operación Data-flow sobre un vector de


longitud n. Una vez se ha desenrollado el bucle por un factor 2, aún quedan n operaciones
mul a lo largo del camino crítico.

Dado que el camino crítico era el factor limitante para el rendimiento del código sin el
desenrollado del ciclo, sigue siendo así con el desenrollado.

21
1.6.3 Data-Flow. Desenrollado de bucles con múltiples acumuladores.

Ejemplo: Desenrollado (x2) con múltiples acumuladores.


for (i = 0 ; i < limit ; i += 2)
{
acc0 = acc0 * x[i];
acc1 = acc1 * x[i+1];
}

Código máquina: i en %rdx, x[i] en %rax, limit en %rbp, acc0/1 en %xmm0/1

.L488:
LOAD %rax, x(%rdx*4) ; Carga x[i]
MUL %xmm0, %rax, %xmm0 ; Multiplica acc0 por x[i]
LOAD %rax, x((%rdx+1)*4) ; Carga x[i+1]
MUL %xmm1, %rax, %xmm1 ; Multiplica acc1 por x[i+1]
ADD $2, %rdx ; Incrementa el índice i
CMP %rdx, %rbp ; Compara el límite i
JG .L488 ; Si >, ir al bucle

En la figura superior hay que notar que no hay dependencia entre las dos
operaciones mul.

La figura adjunta es una representación de la operación sobre un vector de


longitud n. Ahora tenemos dos caminos críticos, cada uno de los cuales contiene
n/2 operaciones. Incluso si sólo tenemos una Unidad Funcional mul, el
paralelismo se puede lograr si la unidad está segmentada, ya que no existe
dependencia entre las dos operaciones mul.

Observamos que cada uno de dichos caminos críticos, uno para los elementos
pares (acc0) y otro para los impares (acc1).

22
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.6.4 Data-Flow. Desenrollado de bucles con reasociación.

Ejemplo: Desenrollado (x2) con reasociación.


for (i = 0 ; i < limit ; i += 2)
acc = acc * (x[i] * x[i+1]); // ven vez de (acc * x[i]) * x[i+1]

Código máquina: i en %rdx, x[i] en %rax, limit en %rbp, acc en %xmm0

.L488:
LOAD %rax0, x(%rdx*4) ; Carga x[i]
LOAD %rax1, x((%rdx+1)*4) ; Carga x[i+1]
MUL %rax0, %rax0, %rax1 ; Multiplica x[i] por x[i+1]
MUL %xmm1, %rax0, %xmm1 ; Multiplica por acc
ADD $2, %rdx ; Incrementa el índice i
CMP %rdx, %rbp ; Compara el límite i
JG .L488 ; Si >, ir al bucle

La figura superior es una abstracción gráfica de las operaciones del data-flow.


Hemos reordenado, simplificado y abstraído la representación para mostrar las
dependencias de datos entre las sucesivas iteraciones (a). El primer operador mul
multiplica los dos elementos del vector, mientras que el segundo multiplica el
resultado por la variable del bucle acc (b).

La figura adjunta representa las operaciones sobre un vector de longitud n.


Tenemos un único camino crítico, pero contiene sólo n/2 operaciones.

La primera multiplicación dentro de cada iteración puede ser realizada sin tener
que esperar al valor acumulado de la iteración anterior.

Esta optimización es difícil de aplicar por los compiladores.

23
Ejercicio propuesto 1:

Dado el siguiente código, correspondiente a un filtro de imagen en blanco y negro,

for ( f = 0; f < 600 ; f++){


for ( c = 0; c < 800 ; c++){
img[f][c] = 255 – img[f][c];
}

a) ¿Qué hace el programa?

El programa invierte los diferentes tonos de grises (255 tonos) mediante la diferencia al complementario.

b) Hallar el flujo de datos.

Nos centraremos en el del bucle interno por ser la parte del proceso más común, suponiendo que las
operaciones de acceso a memoria requieren 3 ciclos y las operaciones aritméticas de 1 único ciclo.

Instrucciones útiles Control del bucle


3 LOAD 1 ADDI
1 SUB 1 CMP
3 STORE 1 BRANCH
∑ 7 ciclos y ∑ 3 ciclos NO es aditivo ! El límite de flujo de datos es de sólo 7 ciclos.

c) Hallar el número de instrucciones mínimo, por iteración, suponiendo que img[f][c] está apuntando a
un registro base. Usar las instrucciones: LOAD, STORE, SUB, ADD, CMP, MOVE0, BRANCH. ¿Cuál de las
instrucciones que se han indicado sobra?

Sobra la instrucción MOVE0, también conocida como CLEAR.

d) Si el proceso se ejecuta en 480 µs y la frecuencia es de 3 GHz, calcular el CPI.


𝑇𝑖𝑒𝑚𝑝𝑜𝑒𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛 𝑇𝑖𝑒𝑚𝑝𝑜𝑒𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛 ∙ 𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎 𝑑𝑒 𝑟𝑒𝑙𝑜𝑗
𝐶𝑃𝐼 = = =
𝑁º𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 ∙ 𝑃𝑒𝑟𝑖𝑜𝑑𝑜 𝑑𝑒 𝑟𝑒𝑙𝑜𝑗 𝑁º𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠

480 ∙ 10−6 𝑠𝑒𝑔𝑢𝑛𝑑𝑜𝑠 ∙ 3 ∙ 109 𝐻𝑧 480 ∙ 10−6 ∙ 3


= = = 266′6̂
6 𝑖𝑛𝑠𝑡𝑟𝑐𝑐 𝑎𝑙𝑔𝑢𝑛𝑎𝑠 𝑑𝑒𝑠𝑝𝑟𝑒𝑐𝑖𝑎𝑏𝑙𝑒𝑠 4800 + 600
(1 𝑖𝑡𝑒𝑟𝑎𝑐𝑖ó𝑛 ∙ 800 𝑐𝑜𝑙𝑢𝑚𝑛𝑎𝑠 + 1 𝑖𝑡𝑒𝑟𝑎𝑐𝑖ó𝑛 ∙ 600 𝑓𝑖𝑙𝑎𝑠)

24
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio propuesto 2:

Supongamos que se realiza una mejora que favorece un determinado modo de ejecución en un factor de 10.
Dicho modo mejorado es usado en un 50% del tiempo medido una vez que el modo de ejecución es aplicado
(nótese que el caso es distinto a la definición habitual). Calcular la aceleración obtenida y el porcentaje de tiempo
de ejecución original convertido a modo rápido.

Consideremos un cambio en el juego de instrucciones de una máquina load/store de la que se dispone de las
siguientes estadísticas:

OPERACIONES FRECUENCIA CPI


ALU 43% 1
Load 21% 2
Store 12% 2
Saltos 24% 2

Supongamos que un 25% de las operaciones tipo ALU usan un operando que no volverá a usarse. Proponemos
añadir instrucciones ALU con un operando fuente en memoria. Estas instrucciones “registro-memoria” duran 2
ciclos. Este nuevo conjunto de instrucciones extendido incrementa la duración de los saltos a 3 ciclos, aunque
no afecta a la duración del ciclo de reloj. ¿Mejoraría esto las prestaciones de la CPU?

Por otro lado, el benchmark “Whetstone” contiene un total de 195.578 operaciones en punto flotante. Se puede
ejecutar sobre un sistema empotrado de bajo consumo (basado en un Motorola 68020 a 16’67 MHz con
coprocesador externo), de dos formas, usando un compilador, que permite usar rutinas software para emular
las operaciones en punto flotante, en lugar de usar el coprocesador.

Se obtienen los siguientes resultados:

 Con software:
- Duración de una iteración = 13’6 segundos.
- CPI = 6.

 Con el coprocesador:
- Duración de una iteración = 1’08 segundos.
- CPI = 10.

Se pide:

a) ¿Por qué es más rápida la ejecución con coprocesador si su CPI es mayor?

b) ¿Cuál es el número total de instrucciones ejecutadas para ambos casos?

c) Calcular los MIPS para cada ejecución.

d) De media, ¿Cuántas operaciones enteras son necesarias para realizar cada operación en punto flotante
en software?

25
Solución:

A partir de las estadísticas descritas en el enunciado, con el nuevo diseño aparecen una instrucción nueva, al
fusionar una Load y una ALU: Por ejemplo para una máquina de tres operandos:

Load R3, [MEM]


pasa a: ALU fuente R1, R2, [MEM]
ALU R1, R2, R3

Es decir, con esta forma, por cada 2 instrucciones de este tipo, se crea una única tipo ALU fuente, decreciendo el
número de instrucciones totales. Como en el 25% de las ALU se puede hacer esto tenemos una frecuencia del
43%, por tanto, 0’43 ∙ 0’25 ≈ 0’11, lo que significa que habría 11 instrucciones serian ALU fuente (desapareciendo
11 ALUs y 11 Loads). Para el nuevo diseño hay que tener en cuenta que la frecuencia de instrucciones cambia,
en concreto aparece un nuevo tipo de instrucción y desaparecen algunas ALU y Load. Se resume el cambio en la
siguiente tabla, donde en la segunda columna se expresa en qué se convertirían las 100 instrucciones de la
máquina anterior.

Número sobre un total de


Instrucciones (j) Frecuencia (Fj)
(100-11=89) instrucciones
Cargas 10 10 * 100 / 89
Almacenamientos 12 12 * 100 / 89
Saltos 24 24 * 100 / 89
ALU operaciones en registros 32 32 * 100 / 89
ALU fuente (operaciones en memoria) 11 11 * 100 / 89

El CPI medio de la máquina original se calcula ponderando los CPI medios de cada una de las instrucciones:
𝑇𝑖𝑝𝑜𝑠 𝑖𝑛𝑠𝑡𝑟
𝑐𝑖𝑐𝑙𝑜𝑠
𝐶𝑃𝐼𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 = ∑ 𝐹𝑗 𝐶𝑃𝐼𝑗 = (0′ 43 ∙ 1′ 0) + (0′ 57 ∙ 2′ 0) = 1′ 57
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛
𝑗=1

El CPI medio de la máquina nueva se calcula igualmente así (cuidado con los porcentajes):
𝑇𝑖𝑝𝑜𝑠 𝑖𝑛𝑠𝑡𝑟
100 𝑐𝑖𝑐𝑙𝑜𝑠
𝐶𝑃𝐼𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 = ∑ 𝐹𝑗 𝐶𝑃𝐼𝑗 = [(0′ 32 ∙ 1′ 0) + (0′ 24 ∙ 3′ 0) + (0′ 33 ∙ 2′ 0)] ∙ = 1′ 91
89 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛
𝑗=1

Para calcular la aceleración entre los dos procesadores, hemos de tener en cuenta los cambios en las CPI y
también en el número de instrucciones, ya que en el diseño nuevo la cantidad de instrucciones disminuye (100
instrucciones originales se convierten en 89):
𝑡𝑒𝑗𝑒𝑐. 𝑛𝑢𝑒𝑣𝑜 𝑇 ∙ 𝑁𝑖𝑛𝑠𝑡𝑟. 𝑛𝑢𝑒𝑣𝑎𝑠 ∙ 𝐶𝑃𝐼𝑛𝑢𝑒𝑣𝑜 𝑇 ∙ 89 ∙ 1′91 170
𝐴 = = = = = 1′ 08 > 1
𝑡𝑒𝑗𝑒𝑐. 𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 𝑇 ∙ 𝑁𝑖𝑛𝑠𝑡𝑟. 𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 ∙ 𝐶𝑃𝐼𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 𝑇 ∙ 100 ∙ 1′57 157

Podemos concluir con indicando que el nuevo diseño es aproximadamente un 8% más lento que el original.

26
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

1.7 Ejercicios.

1. Explicar si son verdad las afirmaciones y razonar:


- Si rendimiento crece, entonces productividad crece igualmente.
- Si productividad crece, entonces rendimiento crece igualmente.

2. Las tarjetas de red antiguas enviaban o recibían datos a una velocidad de 100 Mbit/s. ¿Cuántos años
aproximadamente tardaron en hacerlo a 1 Gbit/s, si el ritmo anual de progreso ha sido del exactamente del
100%? 13’0 años, 0’5 años, 1’1 años, 3’4 años.

3. Una tarjeta de red trasmite información a 1 Gb/s ¿Cuánto tiempo tardará aprox. en enviarse 125 KB? Pista:
al igual que v = espacio/tiempo , usar: v = 1 Gb/s, espacio = 125 KB

a) 125 KB = 125/8 Kb = 15’6 Kb. Como 1 Gb= 106 Kb, tardará 15’6/ 106 s, es decir 15’6 microsegundos.

b) 125 KB = 8 * 125 Kb = 1 Mb. Luego tardará 1000 s, pues hay que dividir la velocidad por el tamaño,
es decir (1 Gb/s) / 1 Mb

c) Ninguna de las otras es cierta.

d) 125 KB = 8 * 125 Kb = 1 Mb. Luego tardará 1 ms, ya que 1 Mb es la milésima de 1 Gb. O bien, si se
divide el tamaño por la velocidad tenemos: 1 Mb / (1 Gb/s) = 0’001 s

4. ¿Cuántos años se tardará en procesar 10 GFLOPS en un PC? Datos: progresión rendimiento anual: 50%. Hoy
en día es de 3 GFLOPs

5. Si una caché está vacía y sólo se sabe que tiene 64 bytes por línea, ¿se puede calcular el MR (razón de fallos)
para este código? Si la respuesta es no, ¿por qué? Si la respuesta es afirmativa ¿cuánto valdrá?
double x[8123];
for (i=0; i< 4096; i++) x[i] = 10.0;

6. Si una caché está vacía y sólo se sabe que tiene 32 Bytes por línea, ¿Cuál es el MR (razón de fallos) si se
recorre un vector para leer todos sus elementos, por ejemplo así:
double x[32*N];
for (i=0; i< 32*N; i++) a += x[i];

7. ¿Qué tamaño completo en Bytes tendrán estos vectores?


double d[8];
float f[1024][1024];

8. ¿Qué tamaño completo en Bytes tendrá esta estructura (sumar Bytes de cada campo)?
struct {
float f;
char c[4];
int i[2];
double d;
};

9. ¿Cuál es la distancia en bytes entre los elementos de la matriz que se dan?


int M[10][10]; Distancia en Bytes
M[0][0] y M[0][1]
M[1][0] y M[0][0]
M[0][0] y M[1][1]

27
10. ¿Cuál de estos programas tiene más localidad de acceso a datos (ambos hacen lo mismo)?
for (f=0; f<m; f++)
for (c=0; c<m; c++)
a[f][c] = a[f][c] + b[f][c];

for (c=0; c<m; c++)


for (f=0; f<m; f++)
a[f][c] = a[f][c] + b[f][c];

11. En arquitectura de computadores, ¿qué significa que una técnica es estática?

- El compilador la realiza.
- El procesador la realiza en tiempo de ejecución.
- El procesador la realiza antes de ejecutar.
- La jerarquía de memoria se encarga de ella.
- Varía a lo largo de los años.

12. En arquitectura de computadores, ¿qué significa que una técnica es dinámica?

- El compilador la realiza.
- El procesador la realiza en tiempo de ejecución.
- El procesador la realiza antes de ejecutar.
- La jerarquía de memoria se encarga de ella.
- Varía a lo largo de los años.

13. ¿La máquina de Von Neumann es esencialmente paralela o secuencial?

14. ¿Por qué MIPS no es lo mismo que rendimiento?

15. ¿Por qué interesa medir el rendimiento con una colección de programas?

16. ¿Qué es un kernel de una aplicación (para medir el rendimiento)?

17. Si dos máquinas son compatibles (ejemplo AMD e Intel) y usan la misma frecuencia, ¿es cierta esta fórmula
de aceleración para una aplicación? A = CPIIntel / CPIAMD

18. El primer bucle tiene un CPI de 0’4, pero es segundo de 5’0. SI ambos tienen el mismo número de
instrucciones y la frecuencia de CPU es la misma ¿Por qué tiene más GFLOPS el segundo?
float x[], y[], z[];
for (i=0; i<1024; i++)
if (x[i] == y[i])
x[i] = z[i];
for (i=0; i<1024; i++)
x[i] = x[i] * y[i] / z[i];

19. Citar grupos de benchmark principales.

20. ¿Qué código es en general más paralelizable: el de programas INT o el de FP?

21. ¿Qué propiedad tienen las medias geométricas para calcular la aceleración para una colección de programas
entre dos máquinas?

22. ¿Cuál es el número de instrucciones en vuelo para un superescalar de 15 etapas y que ejecuta 4 instrucciones
a la vez?

23. ¿Qué puede significar la T de TLP?

24. ¿Cuáles son las principales causas de bloqueo en procesadores GPP actualmente?

28
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2 Paralelismo a Nivel de Instrucciones.

2.1 Conceptos Básicos.

Definimos planificación, secuenciamiento o reordenamiento (scheduling) de un programa a cambiar el orden de


sus instrucciones para evitar bloqueos.

Nuestro objetivo es eliminar riesgos reduciendo los bloqueos. Típicamente se pueden reducir los bloqueos de
datos, ya que los bloqueos estructurales dependen del hardware.

Tenemos que recordar que las dependencias no reales o ficticias las veíamos como, por un lado
antidependencias, WAR (write after write) j escribe un registro que i lee donde el orden inicial (i antes que j)
debe ser preservado. Por otro lado, dependencias de salida WAW (write after write), i y j escriben en el mismo
registro donde igualmente el orden debe ser preservado. Estas dependencias se pueden evitar mediante técnica
de renombrado de los registros. Luego es importante tener un conjunto amplio de registros para poder
renombrar y reordenar (ventaja RISC).

Sin embargo, las dependencias reales RAW (read after write) no pueden eliminarse, son inherentes al algoritmo
(habrá algoritmos con más RAW y otros con menos). Eliminarlas no es tema relativo a la Arquitectura sino a la
Algorítmica.

Por tanto, realmente un programa podría escribirse atendiendo exclusivamente a las dependencias reales, sin
ser necesarios en un primer momento los registros, claro que cuando se diseña un procesador como máquina
síncrona, aparecen obligatoriamente los registros como el estado de la máquina almacenado tras cada periodo.

Nuestro objetivo será intentar que el programa se ejecute como el grafo siguiente:

Ejemplo: a[i] = a2 * a3 / a4 + a3;

Las líneas rojas son dependencias reales.

Las líneas azules y verdes son dependencias


ficticias donde se sustituyen los contenidos.

Las dependencias ficticias son prescindibles, es decir, se


pueden destruir si existen registros libres. Por ejemplo, a
cada registro destino se le asigna uno nuevo
renombrándolo, lo que implica tener que seguirle el
rastro, respetando siempre las dependencias reales.

Este proceso de renombrado de registros se realiza


dinámicamente de forma automática por el ensamblador.
Dichos registros son en realidad punteros (buffers)
llamados Estaciones de Reserva (RS, Reservation Station).

29
En un encadenamiento básico, un bloqueo por cualquier tipo de dependencia lo hace totalmente sobre la cadena
(pipelining), lo que aumenta el CPI de la máquina. Luego, reordenando el código, podemos eliminar ciertos
bloqueos, típicamente los de acceso a los datos.

Pero existe un límite, en la aceleración alcanzable, el límite de CPI (Data Flow Limit, límite del flujo de datos),
que es en sí un límite de las prestaciones de la máquina, donde sólo las etapas EX y MEM son relevantes y las
etapas IF, ID y WB se realizan en paralelo. Los procesadores actuales han llegado prácticamente a ese límite.

Por ejemplo, supongamos que las fases “accesorias” de una instrucción (en las que la instrucción se busca, se
decodifica, etc.) duran cero ciclos, y que únicamente duran un tiempo apreciable las operaciones, entre ellas, el
acceso a la memoria. El grafo de dependencias se convertiría en el siguiente cronograma (sólo fases EX, MEM):

30
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.2 Planificación Dinámica.

2.2.1 Caracterización de la planificación dinámica.

La planificación dinámica consiste en reordenar la ejecución de las instrucciones del código de un programa para
reducir los bloqueos mientras se mantiene el flujo de datos (pipelining), eliminando las dependencias ficticias
tanto WAR como WAW.
El compilador no necesita conocer la microarquitectura.
Ventajas: {
Maneja casos donde las dependencias no se conocen en tiempo de compilación.
Incremento considerable en la complejidad del hardware.
Inconvenientes: {
El control de las excepciones se hace muy complicado.
Ejecución fuera de orden.
La planificación dinámica implica: {
Completado fuera de orden.
Ejecuta las instrucciones sólo cuando los operandos están disponibles.
Algoritmo de Tomasulo: {
Renombra los registros por hardware para eliminar las dependencias WAR y WAW.

2.2.2 Dependencias de datos en memoria.

Los accesos a memoria (ejecutados en búferes de carga y de almacenamiento) pueden tener dependencias entre
los datos que leen o escriben. Para ello debe ocurrir que la dirección de un acceso sea igual a la de otro.
Evidentemente esta dependencia no se puede detectar en tiempo de compilación. Por ejemplo:
SW 20(R3), R9
LW R1, 8(R4) Si ocurre que R3=4 y R4=16, entonces: 20+R3 = 8+R4 provocando RAW en memoria.
En este caso, antes de que una orden LOAD acceda a la memoria caché, se debe comparar su dirección efectiva
con todas las de los búferes de almacenamiento. Si hay coincidencia entonces se detecta una dependencia real
en memoria, y hay que esperar a que la orden STORE termine. De no existir tal comparación de direcciones, no
se podría ejecutar una orden LOAD antes de que acabaran todas las órdenes STORE (caché actualizado). Si bien las
órdenes STORE suelen ser las últimas en ejecutarse.

Los búferes de STORE son como un buffer de escritura (conserva la dirección y el dato escrito). Los datos de las
órdenes STORE se guardan en el buffer y se dan por realizados. Las órdenes LOAD son más prioritarias ya que
puede haber otras instrucciones esperando por sus datos.

En procesadores con pocos registros de usuario (CISC) el código tendrá muchos accesos a memoria, por lo que
el riesgo es mayor y se necesita más hardware para resolverlo. Encontramos dependencias en una instrucción
del siguiente tipo donde, dada una sucesión de operaciones aritméticas, hay que respetar el orden de prioridad:
a[i] = a2 * a3 / a4 + a3;

MULTF F1, F2, F3 Dependencia Real. La división no se puede realizar hasta que no
DIVF F2, F1, F4 termine la multiplicación.
ADDI F1, F2, F3 Dependencia Ficticia. Reutilización del registro F1 sustituyendo
SF (R1), F1 valores.

Nota: El registro F2 es reutilizado, antes era un registro fuente y después un registro destino, esto es debido a
que ya no se vuelve a utilizar y se dispone de pocos registros auxiliares.

31
Hasta ahora todas las técnicas de segmentación ejecutan las instrucciones en orden. Si una instrucción no puede
ejecutarse, la cadena se detiene completamente:

La espera que ADDF introduce en SUBF, ADDI, ... , podría eliminarse si se permitiera que la ejecución (EX) no se
hiciera en orden (out-of-order execution), permitiendo que SUBF y ADDI comiencen antes su ejecución (EX) que
ADDF. Es decir, estamos reordenando la fase EX en tiempo de ejecución (dinámicamente).

La fase de decodificación (ID) siempre debe ir en orden para poder comprobar las dependencias.

Vamos a suponer que se comprueban las dependencias de datos y los riesgos estructurales en una única etapa,
en la ID (o IS, Issue en adelante), por tanto, se puede considerar que debe hacer dos cosas:

- Decodificación y Emisión (Issue): Decodificar la instrucción y comprobar las dependencias entre los
datos, así como los riesgos estructurales (anotándolos de alguna forma).

- Lectura de operandos (Read Operands): Leer los operandos disponibles (no tienen RAW cercana) y
esperar por los operandos que tienen dependencia.

Todas las instrucciones pasan en orden por la etapa ISsue, y a partir de aquí pueden ser detenidas o desviadas a
la Unidad Funcional de ejecución correspondiente. Hay que tener en cuenta que la etapa ISsue no puede
desordenarse, porque entonces no se sabrían cuáles son las dependencias.

Como hemos indicado anteriormente, hay que apuntar la información de las dependencias en algún sitio:

- Si la anotación es una tabla central, el algoritmo se llama marcador centralizado (scoreboard) (1964).
- Si la anotación es distribuida, el algoritmo más usual es el Algoritmo de Tomasulo (1967).
32
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Se hace necesario por tanto, utilizar una serie de entradas en cada Unidad Funcional de la máquina, llamadas
Estaciones de Reserva. A éstas llegan las emisiones (IS) de instrucciones con toda la información de la misma,
también llegan los valores de los operandos fuente, si han podido leerse, o cierta información (etiqueta) que
indica qué operando falta (no por ello se bloquea la cadena).

 Una Estación de Reserva (RS, Reservation Station) es el buffer (registro) donde esperan las instrucciones
emitidas hasta que puedan ser ejecutadas, junto a las ALU’s (Unidades Funcionales). Dicha espera se
debe a la no disponibilidad de todos los operandos fuente (se produce RAW: una instrucción anterior
aún no ha generado algún operando requerido). Las Estaciones de Reserva tienen una serie de campos
para anotar el valor de los operandos fuente, y si es necesario, el valor del resultado de la operación y la
etiqueta asociada al registro destino. Cada Estación de Reserva está a la “escucha” del CDB para capturar
por Bypass los operandos que necesite.

 Los Buffers de Memoria son las Estaciones de Reserva para instrucciones de carga/almacenamiento. Es
el lugar (registro) donde esperan los accesos a memoria emitidos hasta que disponen de todos sus
operandos y el bus de memoria (caché) esté libre. En el algoritmo clásico y en general, hay buffers
separados, unos para instrucciones de carga y otros para las de almacenamiento.

 El Bus Común de Datos (CDB, Common Data Bus) une la salida de las Unidades Funcionales, la carga de
memoria con el fichero de registros, las estaciones de reserva y los buffers. Las Unidades Funcionales
mandan el dato de salida a través del BCD para que lo lean el resto de elementos de la CPU (es decir, se
usa en la fase WB, produciéndose un Bypass). Se trata de un recurso común por el que hay que competir.
En general, las máquinas suelen tener más de un CDB (por ejemplo, uno para enteros y otro para coma
flotante) puesto que el número de WB por cada ciclo puede ser mayor que 1. Cualquier CDB tiene dos
buses: uno para el resultado de una operación (tamaño de 32 líneas para Int/Float, 64 líneas para Double)
y otro para la etiqueta asociada al mismo (tamaño log2 del número de etiquetas).

 Las Etiquetas (tags) son identificadores que se asocian a los registros destino cada vez que se emite una
instrucción. A partir del renombrado de un registro destino con una etiqueta, es ésta la que circula por
toda la CPU (estaciones de reserva, buffers, fichero de registros, CDB, etc.). Una etiqueta estará en uso
desde que la instrucción se emite hasta que su resultado es puesto en el CDB. En el algoritmo clásico, el
nombre de la etiqueta coincide con el de la Estación de Reserva, por ejemplo, si la UF de MULT tiene 3
Estaciones de Reserva, entonces las etiquetas serán MULT1, MULT2, MULT3 (codificadas en binario).

Por tanto, tal como estamos viendo, el renombrado de registros es posible gracias a las Estaciones de Reserva,
que contienen principalmente: la instrucción, valores de los operandos almacenados (cuando están disponibles)
y el número de las Estaciones de Reserva de aquellas instrucciones que suministrarán los valores en espera.

Las Estaciones de Reserva recogen y almacenan un operando tan pronto como esté disponible (no implica
necesariamente el guardado en fichero de registros). Las instrucciones pendientes designan la Estación de
Reserva a la cual envían su salida. Los valores de los resultados son puestos en el bus CDB. Únicamente la última
escritura de un registro modifica el fichero de registros.

Cuando las instrucciones son emitidas, el registro especificado es renombrado con el nombre de la Estación de
Reserva. De hecho, puede haber más Estaciones de Reserva que registros.

Esquema general de una Estación de Reserva (RS):


Estado de la
Estado de la RS Tipo de
Registro Fuente 1 Registro Fuente 2 Registro Destino instrucción
(Libre/Ocupado) instrucción
(IS, EX, WB)

33
2.2.3 Dependencias en el flujo de datos.

Supongamos el siguiente bucle:


for (i=0; i< N; i++) 𝑁−1
s = s * x[i];
𝑠 = ∏ 𝑥𝑖
LF x
s 𝑖=0

s Este bucle no se puede ejecutar en paralelo, luego no es vectorizable.

Supongamos ahora este otro bucle:


for (i=0; i< N; i+=3) Observamos que tampoco es vectorizable, pues tiene hay
{
instrucciones con dependencia de la instrucción anterior. Sólo
s = s * x[i];
s = s * x[i+1]; ganamos en la parte control del bucle, que realizará menos ADDI.
s = s * x[i+2];
}

Este problema se resolverá mediante técnicas estáticas con productos parciales:


for (i=0; i< N; i+=3) LF1 LF2 LF3
{
s1 = s1 * x[i]; x
s2 = s2 * x[i+1];
s3 = s3 * x[i+2];
} x
s = s1 * s2 * s3;

Su camino crítico (critial path) quedaría definido como


x
se muestra en la ilustración adjunta enmarcado en rojo:

Debemos saber que las dependencias ficticias se pueden destruir si existen registros libres. Por ejemplo, a cada
registro destino se le asigna uno nuevo renombrándolo, lo que implica tener que seguirle el rastro, respetando
siempre las dependencias reales. En el código siguiente observamos la técnica del renombrado de registros:
MULTF RS1 , F2 , F3
DIVF RS2 , RS1, F4
Este proceso de renombrado de registros se realiza dinámicamente y de
ADDF RS3 , RS2, F3 forma automática por el ensamblador. Dichos registros son en realidad
SF (R1), RS3 punteros (buffers) llamados Estaciones de Reserva (RS, Reservation Staions).

Supongamos que tenemos el siguiente bucle SAXPY, esto es:


for (i=0; i< N; i++) ← Parte de control del bucle
y[i] = a * x[i] + y[i]; ← Parte útil del bucle

¿Se pueden procesar todas las iteraciones en paralelo? Si es así, entonces diremos que es vectorizable.

Como es vectorizable, vamos a desenrollar por ejemplo 3 iteraciones el bucle:


x y
for (i=0; i< N; i+=3) a LF1 LF2
{
y[i ] = a * x[i ] + y[i ]; x
y[i+1] = a * x[i+1] + y[i+1];
+
y[i+2] = a * x[i+2] + y[i+2];
} SF

34
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.2.4 Grupos de técnicas dinámicas.

Las técnicas más habituales de planificación dinámica se basan en:

 Reordenación, conocido también como secuenciación o scheduling.

Cuenta con un buffer de reordenado que mantiene el resultado de la instrucción entre el completado y
el commit. Este buffer tiene cuatro campos:

- Tipo de instrucción: salto/almacenamiento/registro.


- Campo Destino: número de registro.
- Valor del campo: valor de salida.
- Campo de Ready: ¿Ejecución completada?

El proceso de reordenación implica la modificación de las Estaciones de Reserva, donde la fuente de los
operandos ahora es el buffer de reordenado en vez de las Unidades Funcionales. Así pues, los valores no
son escritos ni en registros ni en memoria hasta que la instrucción es confirmada (commit).

En caso de fallo de la predicción, las entradas especulativas son limpiadas del ROB (Re-Order Bus).

Las excepciones no son reconocidas hasta que no están listas para ser confirmadas (commit).

 Predicción dinámica.

Centrado en los saltos condicionales con una tasa de acierto en torno al 95%.

Utiliza los BTB (Branch-Target Buffers) de predicción del siguiente PC, indexados por el PC actual.

35
 Especulación dinámica.

Se trata de dar un paso más para superar las dependencias de control. Ejecuta las instrucciones de la
rama predicha, pero sólo confirma los resultados si la predicción fue correcta. Esto es, ejecuta el commit
de la instrucción y le permite actualizar el fichero de registros cuando dicha instrucción ya no es
especulativa. Así pues, las ejecuciones son fuera de orden, pero los commit son en orden.

Para llevar a cabo esto, se necesita un hardware adicional para evitar cualquier acción irreversible hasta
que se produce el commit de las instrucciones, como la actualización del estado.

Por tanto, la BTB predice los saltos pero puede fallar, el procesador puede especular y debe deshacer los
cambios en caso de haber fallado con la predicción. A nivel de software, sólo se ejecutan instrucciones
reversibles y, a nivel de hardware, se incluyen elementos que permiten el deshacer (rollback).

Pero cabe preguntarnos ¿Cuánto hay que especular? Una mala predicción reduce el rendimiento y la
potencia de la máquina, respecto a la no especulación, pudiendo causar fallos adicionales (caché, TLB).

La especulación de múltiples saltos complica mucho la recuperación, de hecho ningún procesador puede
resolver múltiples saltos por ciclo.

Debemos tener en cuenta que la especulación mejora la eficiencia energética sólo cuando incrementa
las prestaciones significativamente.

Se requiere de un hardware de especulación para todas las instrucciones, por tanto, todas las escrituras
de MEM o WB no se hacen en el fichero de registros o en la caché de datos, sino que se alojan en un
“búfer de reordenación” (ROB, Re-Order Buffer). Tal como se asignó en la emisión de la instrucción (IS)
una entrada en dicho ROB, el microprocesador tiene en cada ciclo dos “estados”, dos PC, los siguientes:

1. Especulativo: Reflejado en el ROB y en las Estaciones de Reserva. Es empleado para emitir


nuevas instrucciones.

2. Confirmado: Reflejado en los registros. Corresponde a la última instrucción que ha finalizado


correctamente. Es el único que existe para el programador.

En caso de un error en la especulación se elimina el estado especulativo y se mantiene sólo el estado


confirmado.

Las instrucciones se emiten siempre en orden (“in-order issue”). El estado especulativo siempre avanza
con cada emisión. Una instrucción sólo hace “CM, completion” si las instrucciones anteriores ya lo han
hecho. Tenemos que, para toda predicción, si se produce una interrupción, las siguientes situaciones:

1. Si la predicción fue correcta: y las instrucciones anteriores terminaron correctamente y no hubo


interrupciones, entonces se escribe definitivamente en el fichero de registros o en la caché de datos:
se confirma, finaliza, retira o completa la instrucción (fase “commit”, “completion” o “RET retired
instruction”). Se establece un nuevo estado “confirm” del microprocesador, de la misma forma que
si se incrementara el PC.

2. Si la predicción fue incorrecta: o hay interrupción en instrucción anterior, se eliminan las


instrucciones encoladas (posteriores al salto o a la interrupción). Se vuelve al último estado
confirmado: PCespeculativo retorna del PCconfirmado. Como el algoritmo de planificación dinámica permite
ejecución fuera de orden (“out-of-order-execution”), ahora el ROB restablece la finalización en orden
(“in-order-completion”), de ahí el nombre de Búfer de Reordenación (Re-Order Buffer).

36
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.2.5 Algoritmo de Tomasulo.

Robert Marco Tomasulo (1967-2008) fue un científico de la computación y el inventor del


llamado Algoritmo de Tomasulo. Contribuyó en el diseño del IBM 360 desarrollando una
técnica para acelerar las operaciones de coma flotante. Hoy en día casi todos los procesadores
llevan incorporada alguna variante de este algoritmo. Se trata de una técnica de planificación
dinámica de instrucciones con gestión distribuida. Nosotros estudiaremos el algoritmo clásico
pero para todo tipo de instrucciones, porque es el explicado en los libros de arquitectura de
computadores generalmente.

Este algoritmo se puede aplicar a otro tipo de sistemas donde hay ciertos recursos compartidos por diversos
agentes, y se quieren gestionar los recursos de forma distribuida. Evidentemente la implementación que
veremos aquí es únicamente de tipo hardware.

Durante su ejecución provoca un reordenamiento dinámico en la ejecución (fase EX) de las instrucciones, aunque
la emisión sigue siendo en el orden del código original. Además, permite la ejecución de instrucciones no
dependientes de otras anteriores, aunque estas últimas estén bloqueadas esperando por algún operando fuente.

Realizará un renombrado dinámico de registros para evitar riesgos por dependencias (WAR, WAW) entre
instrucciones. Todos los registros de destino se renombran y el nuevo nombre que toman se llama etiqueta (tag).

En este algoritmo se ponen una serie de entradas, las Estaciones de Reserva, en cada Unidad Funcional. A éstas
llegan las emisiones (IS) de instrucciones con toda la información de las mismas, también llegan los valores de
los operandos fuente, si han podido leerse, o cierta información en forma de etiquetas (tags) que indica qué
operando falta (no se bloquea la cadena). Cada Estación de Reserva está a la “escucha” del CDB para capturar
por bypass los operandos que necesite.

En la implementación del Algoritmo de Tomasulo, las Estaciones de Reserva son registros asociados a las
Unidades Funcionales, de manera que forman una pequeña cola de instrucciones a la entrada de la Unidad
Funcional (de unas pocas por Unidad Funcional si la operación no es muy común, como la división DIV, a muchas
si es muy frecuente). Cada registro contendrá la información necesaria para la ejecución de la instrucción y para
la llegada de los operandos que le falten, en caso de que la instrucción esté en espera.

37
2.2.5.1 Etapas del Pipeline en el Algoritmo de Tomaluso:

Se puede definir la especificación exacta (en lenguaje C o con simuladores) del Algoritmo de Tomasulo. Que se
implementa posteriormente sobre hardware.

Dado que con la arquitectura hardware de dicho algoritmo la cadena no se bloqueará cuando haya RAW, la
cadena (pipelining) debe cambiar.

 Fetch (IF): Acceso a la memoria caché de instrucciones para leer la instrucción apuntada por el PC.

 Issue (IS): Emisión de la instrucción. En el algoritmo clásico esta etapa se hace en un sólo ciclo (hoy en
día esta etapa dura varios ciclos de reloj). Realizaría lo siguiente:

- Decodificación de la instrucción.
- Lectura de los operandos que ya estén disponibles.
- Envío hacia la Estación de Reserva, la cual sería la auténtica emisora.

Al final de esta etapa IS, se intenta enviar la instrucción a una Estación de Reserva (buffer de
carga/almacenamiento), asignando una etiqueta (tag) al registro destino.

- Si hay una Estación de Reserva libre en la Unidad Funcional requerida por la operación entonces
se emite la instrucción mandando a la Estación de Reserva los operandos disponibles. Para los
que no estén disponibles, se anota la etiqueta por la que esperan.

- Si no hay una Estación de Reserva libre o no quedan etiquetas (tag) libres, la etapa IS se
bloqueará hasta que una Estación de Reserva de la Unidad Funcional se libere. En el algoritmo
clásico, y siempre que todo se realice en una única fase IS, esto provoca una detención de la
cadena de ejecución.

 Execution (EX): Cuando una Estación de Reserva obtiene todos sus operandos, empieza a ejecutarse, por
lo que la primera instrucción preparada es la primera que se ejecutará. La Estación de Reserva puede
quedar libre cuando comience a ejecutarse en la Unidad Funcional el primer EX1, entonces, esta etapa
conservará en algún otro sitio dos campos: uno para el resultado y otro para la etiqueta asociada al valor
de salida enviándose en WB junto al resultado de la operación. Igualmente, también puede quedar libre
al final de la etapa WB. La Estación de Reserva tendrá todos los campos necesarios para operandos
fuente, resultado y etiqueta.

 Write Back (WB): El resultado de una operación y su etiqueta se escriben en el CDB, de manera que las
Estaciones de Reserva que estén esperando por este valor (tienen la misma etiqueta en alguno de sus
operandos) lo toman, es decir, se produce un bypass o camino de desvío a través del CDB. Igualmente,
el valor del CDB se escribe en el fichero de registros de coma flotante si su etiqueta coincidiera con
alguno. La etiqueta se libera tras la escritura. El fichero de registros deberá, por tanto, tener información
sobre la última etiqueta que va asociada a cada registro.

Es necesario un arbitrador de cada CDB, ya que si varias Estaciones de Reserva quieren escribir a la vez
en un CDB, el arbitrador dará permiso sólo a una, a la más antigua en su emisión.

NOTA: La fase MEM no existe, sino que se considera dentro del EX para los buffers de memoria.

38
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Resumen de la ejecución del Algoritmo de Tomasulo.

 Emisión: Obtiene la siguiente instrucción de una cola FIFO y, si hay Estaciones de Reserva disponibles
entonces le emite la instrucción con los valores de los operandos que estén disponibles en ese momento; y
si no la hay o no está disponible algún operando, envía los datos de la Estación de Reserva que lo generará.
Pero, si ya no quedase alguna Estación de Reserva disponible entonces bloquea la instrucción.
 Ejecución: Si algún operando no está disponible, monitoriza el CDB. Cuando el operado aparece por el CDB,
lo guardan las Estaciones de Reserva que estuvieran esperándolo. Cuando todos los operandos necesarios
están disponibles (almacenados en la RS), se ejecuta la instrucción. Las cargas y los almacenamientos deben
ser ejecutadas en el orden correcto, vigilando la dirección efectiva. No se permite iniciar la ejecución de
instrucciones hasta que todos los saltos que la preceden, en orden natural del programa, se hayan resuelto.
 Escritura de Resultados: Escribimos (almacenamos) los resultados sobre el CDB, las Estaciones de Reserva,
buffers de almacenamiento y, en su caso, el Fichero de Registros, que necesiten dicho dato. Los
almacenamientos deben esperar a recibir tanto la dirección como el valor a almacenar.

2.2.5.2 Variaciones del Algoritmo de Tomasulo.

La implementación del Algoritmo de Tomasulo en las máquinas actuales sigue la misma idea principal del
algoritmo visto, pero se puede encontrar con distintas modificaciones o adaptaciones. Antiguamente se aplicaba
este algoritmo sólo para instrucciones largas en coma flotante, código para supercomputadores científicos. De
forma que la cadena era distinta para coma flotante que para enteros. Actualmente las operaciones enteras
también llevan planificación dinámica similar al Algoritmo de Tomasulo. En el Algoritmo Clásico, la etiqueta y las
Estaciones de Reserva son lo mismo. Se supone una etiqueta por cada Estación de Reserva, luego para poder
emitir, el requisito es único: ¿hay alguna Estación de Reserva disponible? Cuando la Unidad Funcional termina la
ejecución, se devuelve el resultado a la Estación de Reserva, ésta pondrá el resultado en el CDB cuando este se
encuentre disponible, liberándose la Estación de Reserva y por ende, la etiqueta.

Existen arquitecturas con Estaciones de Reserva que se liberan al empezar la fase EX (cuando todos los operandos
fuente están disponibles). Cuando la Unidad Funcional termina la ejecución, se recoge el resultado de la
operación en otro registro interno. En cuanto haya un CDB disponible se libera tal registro interno. También
existen arquitecturas con “pilas de etiquetas” (tag-pools). Las etiquetas son un recurso común y compartido.
Para emitir una instrucción se precisa un requisito doble: debe existir una etiqueta disponible y una Estación de
Reserva libre (la etiqueta tiene asociado un elemento hardware). El registro destino se renombra con la etiqueta
(y no coincide con la Estación de Reserva). Un ejemplo usual de pila de etiquetas donde las Estaciones de Reserva
se liberan en EX, se da en los micros que renombran, dinámicamente y para todas las instrucciones emitidas, los
registros lógicos (de usuario, de las instrucciones de ensamblador) por registros físicos (internos, no visibles al
programador). Los registros físicos funcionan como etiquetas. Suele haber muchos más registros físicos que
lógicos, por ejemplo: Power PC, MIPS.

El conjunto de Estaciones de Reserva puede ser:


 Común para todas las Unidades Funcionales.
 Separadas en Estaciones de Reserva para enteros (INT) y otras para coma flotante (FP).
 Cada Unidad Funcional tiene sus Estaciones de Reserva asociadas y propias (como el Tomasulo clásico).

Todos tienen varios CDB para permitir más de una escritura al mismo tiempo. En algunos micros, la fase WB no
existe, en el último ciclo de ejecución se escribe en el CDB, tal como se hace en el DLX sin planificación dinámica,
el desvío de un dato por un bypass no tarda ningún ciclo. Así se acercan al límite flujo de datos. La cadena tiene
diferente número de etapas (no es la clásica IF IS EX WB). Generalmente la fase IS se subdivide en varias subfases,
pues hay que hacer muchas operaciones: decodificación, lectura de operandos, renombrado, emisión, etc.

39
2.2.5.3 Casos de estudio del Algoritmo de Tomasulo.

Tenemos los siguientes tres casos:

 Caso 1: Instrucciones con todos los operandos disponibles, se ejecutan sin esperas, como un pipeline.
Sería el caso trivial, donde tiene un CPI ideal. La sucesión de fases sería:
IF → IS → EX → WB

 Caso 2: Instrucciones con dependencia real de otra instrucción anterior. Un ejemplo sería:

IF IS ○ ○ ○ ○ ○ EX WB a la espera del operando que se necesitaba!


Lee 0 o más operandos, pero le falta alguno de ellos (5 fases con bloqueo).

 Caso 3: Las Estaciones de Reserva están agotadas y las instrucciones no pueden anotar su información
en alguna de ellas, lo que provoca un bloqueo por desconocer la información de la instrucción anterior.
IF - - - IS EX WB

3 fases con bloqueo

2.2.6 Motivos que limitan el ILP Dinámico.

Los sistemas superescalares se han acercado al pleno límite del flujo de datos (Data Flow Limit), viéndose
restringido por las características del propio código de la aplicación.

 ¿Qué dos factores limitan más el paralelismo extraíble en los benchmarks reales?

1. Fallos en la predicción de saltos (condicionales tipo if-else difíciles de predecir).

2. Accesos a memoria de datos (fallos de caché, consumo de ancho de banda de RAM masivo).

 Otros factores secundarios son:

3. Dependencias reales sucesivas (sumatorios).

4. Uso de operaciones que no están segmentadas (operación de división DIV).

5. Uso de las mismas operaciones (código conteniendo sólo operaciones aritméticas).

El programador es el encargado de reducir estos factores, esto es, de simplificar los patrones de acceso y del
comportamiento de los saltos.
40
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Motivo 1. “Fallos en la predicción de saltos (condicionales tipo if-else difíciles de predecir)”.

Nos encontramos en el caso como se ilustra en el siguiente código:


BRANCH RC, ETQ, Q
INSTR. SIGUIENTE_1
INSTR. SIGUIENTE_2 IF IS ○ ○ ○ EX WB
INSTR. SIGUIENTE_3 IF IS EX WB
INSTR. SIGUIENTE_4 IF IS ○ Resuelve el salto
IF IS
ETQ: INSTR. DESTINO_1
IF IS
INSTR. DESTINO_2
INSTR. DESTINO_3
( ... )

Número de ciclos de bloqueo


Cálculo de la penalidad: Penalidad = Número de fallos

 Existen dos técnicas de predicción de saltos:

 Información local de cada salto por separado (BTB simple).


 Información histórica global acumulada de todos los saltos.

Ejemplo 1:

for (i=0; i<5; i++) tendríamos la siguiente correlación: F → F → F → F → (V ⇒ salto)

Ejemplo 2: Existe una correlación entre el if y el while antes de los saltos. No se sabe si se deberá ejecutar el
bloque siguiente o el de destino.
flag = 0;
while (flag == 0)
{
// operaciones…
if (condición) { flag = 1; }
}

Contamos con la caché predictiva BTB (Branch Target Buffer) que está unida a la memoria caché y accede a las
instrucciones (32 bits) en la fase IF, que predice el siguiente PC. Trata con el IF predicho, de forma que, si acierta
en la predicción, funciona bien, dando lugar a un CPI ideal.

Se nos plantea por tanto dos casos, si acierta en la predicción, entonces el CPI bloqueo = 0 pero, si falla (se detecta
salto en la fase EX), nuestras predicciones no habrán servido de nada. Lo óptimo sería indicar el IF en la siguiente
instrucción después de EX para poder contar los números de ciclos de bloqueo, contar la fase de la última válida
y la actual válida, esto es, IF primera e IF última. En el ejemplo 1 anterior, el número de ciclos de bloqueo es 5.

Nota: En los sistemas superescalares, donde m = 4, su fase más lenta es IS (por la planificación dinámica). En
equipos con arquitectura RISC, la fase IF cuenta con 8 ∙ 32 = 256 bits, por lo que desde la caché de instrucciones
manda 256 bits a la CPU. En la actualidad, el porcentaje de fallos es pequeño, con una media en torno al 4%.
1
Si el bucle anterior tuviera 100 iteraciones y salta en la última, tendríamos un porcentaje de saltos = 100
.

4 Fallos ≈ 10 ciclos de bloqueo 10 (int), 15(float) Número de Saltos


CPI bloqueocentral = ∙ ∙

100 iteraciones ⏟ 1 Fallo ⏟ 100 Instrucciones ejecutadas
% de fallos Penalidad Frec. de saltos del programa
% Saltos

41
Motivo 2. “Accesos a memoria de datos (Fallos de caché, Consumo de ancho de banda de RAM masivo)”.

Caso: Fallos de caché.

Nos encontramos con dos posibles situaciones:

 Acierto en la caché L1, que sería el caso ideal, teniendo en cuenta la duración de acierto de dicha caché.
 Fallo en la caché L1, es el caso menos probable, con el mismo ratio o porcentaje de fallos.

Ejemplo:
for(i=0; i<1000; i++) { Tendríamos la siguiente correlación:
for (j=0; j<3; j++) {
// bucle interno F→F→F→V →F→F→F→V→F→F→F→V
}
// instrucciones del bucle externo como el bucle interno sigue ese patrón
}
se acierta casi siempre.

No obstante, la BTB funciona mejor con un esquema de predicción


Global. Pero la mejor técnica es la conocida como BTB “por torneo” Info. local →
(tournament predictor). Estos predictores adaptativos utilizan un

selectora
Máquina
Predicción 1
esquema de predicción local y otro global, y seleccionan la → Predicción
predicción de uno de ellos según el estado de un contador de Info. global →
saturación similar existente en los predictores simples, en los que se Predicción 2
requiere un cierto número de fallos consecutivos para cambiar de
estrategia.

En caso de acierto, el pipeline contempla esas fases y se permite la segmentación. De este modo tendríamos:
IF IS M1 M2 M3 WB
IF IS M1 M2 M3 WB
IF IS M1 M2 M3 WB

Cuando se produce un fallo en la caché L1, podemos determinar el “Miss rate” o “% fallos”, aunque estos suelan
ser pocos. Veamos, mediante un ejemplo, un fallo de caché:
LOAD IF IS EX Acceso a caché L2 WB
IF IS ○ ○ ○ ○ ○ ○ ○ EX WB
IF IS ○ ○ ○ ○ ○ ○ ○ ○ EX WB
Si hay pocos fallos, hay instrucciones IF IS ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB
IF IS EX WB
independientes que pueden seguir IF IS EX WB
trabajando…

Caché L1 ≈ 3 a 5 ciclos
Caché L2 ≈ 10 ciclos
A saber, los tiempos de acceso a las memorias: {
Caché L3 ≈ 20 ciclos
RAM ≈ 100 ciclos

Tomando el ejemplo anterior, si en vez de tener que acceder a caché L2, se accediera a RAM, aproximadamente
tendrían 400 instrucciones en espera, se llegaría al probable agotamiento de las Estaciones de Reserva y con ello,
al bloqueo del sistema.

42
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Caso: Consumo de ancho de banda de RAM masivo.

Se produce sobre estructuras de grandes volúmenes de datos (vectores), que no caben en la memoria caché,
por lo que se producen fallos forzosos en los accesos a RAM. El ancho de banda de la RAM (ABRAM) es de 20 GB/s

Ejemplo: Partimos de la premisa que tenemos una RAM lenta. Si el tiempo de ejecución es por acceso a RAM…

a) ¿Cuánto tiempo tardan 106 iteraciones de un bucle DAXPY?

Tenemos en cuenta que el bucle DAXPY utiliza variables tipo Double, esto es, requiere 64 bits (8 Bytes).
Así mismo, si se accediera en tres ocasiones a memoria, el tiempo de ejecución sería:

Bytes accedidos ∙ Iteraciones ∙ AccesosMemoria 8 ∙ 106 ∙ 3 '


Tiempoejecución = = 9 = 1 2 ms
Ancho de banda de la RAM 20∙10

b) ¿Cuántos ciclos/iteración tendrá si la frecuencia es de 4 GHz? ¿Dichos ciclos/iteración son mayores que los
ciclos ideales por iteración? Tenemos,

8∙3 Bytes 1 segundo 4∙109 ciclos ciclos DAXPY = 9 instrucciones 9


∙ ∙ = 4' 8 ; instrucc. ⟩ = = 2' 25 iteraciones.
⏟ ⏟
1 iteración 20∙109
Bytes ⏟1 segundo iteración Idealmente (m=4 ) 4
Bytes totales
ciclo
Ancho de banda Frecuencia
accedidos de la RAM de reloj

Según estamos estudiando, podemos concluir que, la predicción de saltos es la alternativa dinámica, en tiempo
de ejecución, para reducir los bloqueos por dependencias de control. Contamos con diversos predictores:

 Predictor básico de 2-bit: Para cada salto predice tomado o no tomado y, si la predicción es errónea dos
veces consecutivas, cambia la predicción.

 Predictor con correlación: Puede ser bien mediante el uso de múltiples predictores de 2-bit para cada
salto, o bien, uno para cada combinación posible de los resultados de las anteriores n ramas.

 Predictor local: Puede ser bien, como en el caso anterior, mediante el uso de múltiples predictores de
tipo 2-bit para cada salto, o bien, uno para cada combinación posible de los resultados de las anteriores
n ocurrencias de este salto.

 Predictor por Torneo: Combina un predictor con correlación con otro predictor local.

Comparativa de las prestaciones de diversos predictores de salto


43
Motivo 3. “Dependencias reales sucesivas (sumatorios)”.

Veamos un ejemplo dado el siguiente código ensamblador:


ADD R1, R1, R1 IF IS1 EX WB1 ← Libera RS
ADD R1, R1, R1 IF IS2 ○ EX WB2 ← Libera RS
ADD R1, R1, R1 IF IS3 ○ ○ EX WB3 ← Libera RS
ADD R1, R1, R1 IF IS1 ○ ○ ○ EX WB1 ← Libera RS
ADD R1, R1, R1 IF - IS2 ○ ○ ○ EX WB2 ← Libera RS

ADD R1, R1, R1 - IF - IS3 ○ ○ ○ EX WB3

1 ciclo 2 ciclos
Pendiente = 1 ⇒ CPI = 1 instrucción = 1 Pendiente = 2 ⇒ CPI = 1 instrucción = 2

Nos preguntarnos ¿cuándo se bloquea el Algoritmo de Tomasulo? Pues al encontramos con un bucle de tipo:

…que se soluciona mediante el renombrado de


for ( . . . i . . . ) la parte que “no importa” de la siguiente forma:
sum = sum + . . . ;
ADDF F8, F8, F05
ADDF F8, F8, F10
Bloqueo No importa ADDF F8, F8, F15
ADDF F8, F8, F20
ADDF F8, F8, F25

Supongamos que tenemos hasta un máximo de 6 Estaciones de Reserva y 3 pipelines…


ADDF F8, F8, F05 IF IS1 EX WB1
ADDF F8, F8, F05 IF IS2 ○ ○ EX WB2
ADDF F8, F8, F05 IF IS3 ○ ○ ○ ○ EX WB3
ADDF F8, F8, F05 IF IS4 ○ ○ ○ ○ ○ EX WB4
ADDF F8, F8, F05 IF IS5 ○ ○ ○ ○ ○ ○ ○ EX WB5
ADDF F8, F8, F05 IF IS6 ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB6
ADDF F8, F8, F05 IF - IS1 ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB1
ADDF F8, F8, F05 IF - - - IS2 ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB2
ADDF F8, F8, F05 IF - - - - - IS3 ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB3
ADDF F8, F8, F05 - IF - - - - - IS4 ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB4
ADDF F8, F8, F05 - IF - - - - - - - IS5 ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB5
ADDF F8, F8, F05 - IF - - - - - - - - - IS6 ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB6

El CPI real lo encontramos en el estacionario.

1 ciclos 1 2 ciclos
CPIideal = = CPIreal = =2
3 instrucciones 3 1 instrucción

sabiendo que, CPIbloqueo = CPIreal - CPIideal

1
Entonces, CPIbloqueo = 2 - = 1'6̂
3

44
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Motivo 4. “Uso de operaciones que no están segmentadas (operación de división DIV)”.

Debemos tener en cuenta que las Unidades Funcionales sí están segmentadas, lo que permite el pipeline, tal
como se muestra:
IS EX1 EX2 EX3 WB
IS EX1 EX2 EX3 WB
IS EX1 EX2 EX3 WB
← Pipeline (forma normal)

Las Unidades Funcionales de las divisiones, por ser una operación muy compleja, están formada por elementos
combinacionales que no permiten la segmentación en el pipeline. Ante ello, podemos evitar las divisiones
multiplicando por sus valores inversos. El siguiente cronograma consta de 6 Estaciones de Reserva y 3 pipelines:

DIVF IF IS1 D1 D2 D… D20 WB1


DIVF IF IS2 ○ ○ ○ ○ ○ D1 D2 D… D20 WB2
DIVF IF IS3 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ D1 D2 D… D20 WB3
DIVF IF IS4 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ D1 D2 D… D20 WB4
DIVF IF IS5 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ D1 D2 D… D20 WB5
DIVF IF IS6 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ D1 D2 D… D20 WB6
DIVF IF - - - - IS1 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ D1
DIVF IF - - - - - - - - - IS2 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○
DIVF IF - - - - - - - - - - - - - - IS3 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○
DIVF - - - - IF - - - - - - - - - - - - - - IS4 ○ ○ ○ ○ ○ ○ ○ ○ ○ ○
DIVF - - - - IF - - - - - - - - - - - - - - - - - - - IS5 ○ ○ ○ ○ ○
DIVF - - - - IF - - - - - - - - - - - - - - - - - - - - - - - - IS6

20 ciclos
CPIreal = = 20 que coincide con la duración de la Unidad Funcional.
1 instrucción

Nota: No se trata de una dependencia real sino de una dependencia estructural.

Motivo 5. “Uso de las mismas operaciones (código conteniendo sólo operaciones aritméticas)”.

Es escasa la probabilidad de encontrarnos con un código conteniendo únicamente operaciones aritméticas


donde ninguna de ellas sea de coma flotante, y que además no realice accesos a memoria. Esto es, una sucesión
de instrucciones aritméticas enteras. Lo podemos encontrar ocasionalmente en algunos problemas de cálculo.

Veamos un ejemplo, con sólo sumas enteras, con los siguientes parámetros: Grado de superescalaridad m = 4,
número de Unidades Funcionales para la operación suma entera, 2. Tenemos entonces:

INT001 IF IS1 EXA WB1


INT002 IF IS2 EXB WB2
INT003 IF IS3 ○ EXA WB3 1 ciclo 1
INT004 IF IS4 ○ EXB WB4 CPIideal = =
INT005 IF IS1 ○ EXA WB1
4 instrucciones 4
INT006 IF IS2 ○ EXB WB2
INT007 IF IS3 ○ ○ EXA WB3
INT008 IF IS4 ○ ○ EXB WB4 1 ciclo 1
INT009 IF IS1 ○ ○ EXA WB1 CPIreal = =
INT010 IF IS2 ○ ○ EXB WB2 2 instrucciones 2
INT011 IF IS3 ○ ○ ○ EXA WB3
INT012 IF IS4 ○ ○ ○ EXB WB4

Observamos que es imposible acelerar más, dondeelritmo


ritmode
deliberación
ejecución es menor, determinado por la fase IS,
y que el ritmo de ejecución es mayor hacia la fase WB.

45
2.3 Planificación Estática.

2.3.1 Conceptos básicos.

Las técnicas dinámicas están empotradas on-chip, de forma transparente al usuario, por lo que la mejora del
resultado se consigue en el tiempo empleado por sus componentes de carácter generalista (General Purpose
Process). Se trata de procesadores dinámicos superescalares (Dynamic Superscalar Processors) pero que
consumen mucha energía.
Respecto a las técnicas estáticas podemos indicar que dependen del programador, lo que requiere el uso
correcto de los “flags” del compilador que utilice; esto es, se basan en ajustes muy finos y detallistas ya que son
altamente dependientes de la arquitectura interna del microprocesador (endo-arquitectura). La sincronización
entre microprocesador y órdenes a ejecutar mantiene una relación muy débil con las diferentes versiones de
este, lo que puede obligar a recompilar todo el código para una nueva versión de microprocesador. Por tanto es
la técnica más utilizada para sistemas empotrados o dedicados, ya que además, consumen menos energía.

Las técnicas estáticas están soportados por procesadores estáticos superescalares (Static Superscalar Processors)
que no cuentan con el Algoritmo de Tomasulo, entre los que podemos destacar los VLIW que reducen el número
de instrucciones para disminuir el tiempo de ejecución. En realidad cuentan con macro instrucciones, por lo que
al final, realizan muchas operaciones, pero que pueden ser independientes y ejecutar muchas de ellas de forma
concurrente. Para encontrar cuáles son factibles y evitar las dependencias reales, se necesita de una planificación
estática avanzada que las reordene y se evite sus bloqueos.

Esquema temporal del funcionamiento de los procesadores VLIW


46
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.3.2 Planificación estática básica.

Consiste en la reordenación de instrucciones dentro de un bloque de código básico. Entendiendo como boque
de código básico al conjunto de instrucciones que no contenga alguna de tipo salto. En lenguaje C se identifican
con aquellas encerradas entre los caracteres { y }.

Ejemplo con un bucle DAXPY: ⃗ = 𝑎 ∗ 𝑋 + 𝑌


𝑌 ⃗ que es paralelizable y vectorizable.
LD FX, 0(R1) ; carga X[i]
LD FY, 0(R2) ; carga y[i]
Reubicar este código aquí
MULTD Fm, Fa, FX ; fm ← a * x[i]
aprovecharía los ciclos de ADDD FY, Fm, FY ; y[i] ← (a * x[i]) + (y[i])
bloqueo de la instrucción SD 0(R2), FY ; almacena y[i]
anterior MULD. -----------------
ADDI R1, R1, 8 ; incrementa el índice x
ADDI R2, R2, 8 ; incrementa el índice y
Overhead
SGT R3, R1, RfinX ; test por si ha finalizado
BEQZ R3, bucle ; volver al bucle si no ha finalizado

La reubicación del fragmento de código indicado en el ejemplo, implica ciertos ajustes en los registros. Debemos
tener en cuenta que, concretamente en el bucle DAXPY, utilizamos números tipo Double, por lo que la variación
del incremento en dichos registros será de 8 en 8.

Por tanto, la reordenación indicada quedaría así: Bypass

LD FX, 0(R1) IF IS EX WB
LD FY, 0(R2) IF IS EX WB
MULTD Fm, Fa, FX IF IS ○ EX1 EX2 EX3 EX4 WB
ADDI R1, R1, 8 IF IS EX WB
ADDI R2, R2, 8 IF IS EX WB
SGT R3, R1, RfinX IF IS EX WB
ADDD FY, Fm, FY IF IS ○ ○ EX1 WB
SD 0(R2), FY IF IS ○ ○ ○ EX WB
BEQZ R3, bucle IF IS EX WB

2.3.3 Planificación estática avanzada.

La planificación estática avanzada consiste en la utilización de diversas técnicas:

 Eliminar los saltos condicionales.

 Desenrollado del código de bucles.

 Software pipelined.

 Ejecución predictiva o condicional.

47
2.3.3.1 Desenrollado del código de bucles.

Tengamos presente que los bucles representan más del 90% del tiempo de ejecución en un procesador.

Veamos cómo desenrollar los bucles paralelizables (vectoriazables) mediante iteraciones independientes.

Ejemplo:

Sea el siguiente código inicial, donde M >>:


double s, x[M] , y[M];
int i;
for (i=0 ; i<M ; i++ )
y[i]= x[i] * s ;

Desenrollamos el bucle en alto nivel:


for (i=0 ; i<M%3 ; i++)
y[i]= x[i] * s;
for ( ; i<M ; i+=3 ) {
y[i+0]= x[i+0] * s ;
y[i+1]= x[i+1] * s ;
y[i+2]= x[i+2] * s ;
}
}

Pasos para el desenrollado de bucle en bajo nivel:

Paso 1:
Bucle_original:
LD F2, 0(R1) ; Instrucciones útiles
MULTD F4, F2, F24 ; F24 = s
SD (R3)0, F4
;---------------------------
ADDI R1, R1, 8 ; instrucciones de overhead (Control del bucle)
ADDI R3, R3, 8
SLTI R7, R1, fin_array_x ; Puntero al final de x[M]
BNEZ R7, Bucle_origianl

Casi no existen dependencias entre instrucciones "útiles" e instrucciones de "overhead" por lo


que reordenamos.

Paso 2:
Bucle_intermedio_2: ; Desenrollando el bucle principal
LD F2, 0(R1)
MULTD F4, F2, F24
SD (R3)0, F4
LD F2, 8(R1)
MULTD F4, F2, F24
SD (R3)8, F4
LD F2, 16(R1)
MULTD F4, F2, F24
SD (R3)16, F4
;----------------------
ADDI R1, R1, 8*3
ADDI R3, R3, 8*3
SLTI R7, R1, fin_array_x
BNEZ R7, Bucle_intermedio_2

48
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Paso 3: El renombrado de registros evita las dependencias WAR, WAW. Notaremos (’) para simplificar el código.
Bucle_intermedio_3:
LD F2, 0(R1)
MULTD F4, F2, F24
SD (R3)0, F4

LD F2’, 8(R1)
MULTD F4’, F2’, F24
SD (R3)8, F4’

LD F2’’, 16(R1)
MULTD F4’’, F2’’, F24
SD (R3)16, F4’’
;----------------------
ADDI R1, R1, 8*3
ADDI R3, R3, 8*3
SLTI R7, R1, fin_array_x
BNEZ R7, Bucle_intermedio_3

Paso 4: Planificamos juntas las instrucciones de las diferentes iteraciones.


Bucle_desenrollado:
LD F2, 0(R1)
LD F2’, 8(R1)
LD F2’’, 16(R1)

MULTD F4, F2, F24


MULTD F4’, F2’, F24
MULTD F4’’, F2’’, F24

SD (R3)0, F4
SD (R3)8, F4’
SD (R3)16, F4’’
;----------------------
ADDI R1, R1, 8*3
ADDI R3, R3, 8*3
SLTI R7, R1, fin_array_x
BNEZ R7, Bucle_desenrollado

Paso 5: Tenemos la opción de poner instrucciones después de aquellas de mayor duración para evitar bloqueos.
Bucle_desenrollado_opcional:
LD F2, 0(R1)
LD F2’, 8(R1)
LD F2’’, 16(R1)

MULTD F4, F2, F24


MULTD F4’, F2’, F24
MULTD F4’’, F2’’, F24

ADDI R1, R1, 8*3

SD (R3)0, F4
SD (R3)8, F4’
SD (R3)16, F4’’
;----------------------
SLTI R7, R1, fin_array_x ;

ADDI R3, R3, 8*3


BNEZ R7, Bucle_desenrollado_opcional

49
Veamos otro ejemplo con los siguientes bucles:
for (i=0; i<N; i+=2)
{
y[i] = a * x[i] + y[i];
y[i+1] = a * x[i+1] + y[i+1];
}
for (i=i; i<N; i++)
{
y[i] = a * x[i] + y[i];
}

Quedaría en bajo nivel como sigue, tonamos con un super-índice el número de la iteración (i + super-índice):

Código desensamblado original Código desensamblado reordenado


DAXPY: LD0 DAXPY: LD0
LD0 LD1
MULTD0 LD0
ADDD0 LD1
SD0
MULTD0
LD1 MULTD1
LD1
ADDD0
MULTD1
ADDD1
ADDD1
SD1 SD0
SD1
ADDI Rbx, Rbx, 16
ADDI Rby, Rby, 16 ADDI Rbx, Rbx, 16
Overhead CMP Rbx, RmaxX ADDI Rby, Rby, 16
BRANCH Rbx, DAXPY Overhead CMP Rbx, RmaxX
BRANCH Rbx, DAXPY

* Se indica un valor de 16 debido a que la iteración del bucle es cada 2 saltos (i+=2) de tipo doublé (8+8 = 16)

Evidentemente, el máximo rendimiento lo tendríamos si desenrollamos las “infinitas” iteraciones del bucle.

Otro aspecto muy importante es que dado que el recuento de instrucciones se reduce, el CPI no es una buena
ciclos
medida del rendimiento, en tal caso deberíamos utilizar: tamaño del vector

Existen bucles que resultan muy complejo su desenrollado, por ejemplo cuando el número de iteraciones es
totalmente desconocido en tiempo de compilación, lo que provoca que el compilador no llegue a realizar el
desenrollado. Tal es el caso de cualquier cálculo científico iterativo, como el de determinar un rango de error:
while(error > ε) {
Xk+1 = f(Xk, entrada);
error = || XK+1 – Xk ||;
}

50
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio:

Sea un bucle DAXPY de 1.000 iteraciones, se pide calcular:

a) Porcentaje de saltos.
b) Número de instrucciones ejecutadas.

Téngase en cuenta que si desenrollásemos las k iteraciones, este valor de k debería ser múltiplo de
las 1.000 iteraciones.

Solución:

Para el caso de un bucle DAXPY original tendríamos:


5 Instrucciones útiles
+ 4 Instrucciones de overhead
1
Porcentaje de saltos = ≅ 11%
9
Instrucciones
9 ⇒
iteración Instrucciones ejecutadas = 1000 ∙ 9 ⇒ ⏟
5000 + ⏟
4000
instrucciones instrucciones
{ útiles overhead

Para nuestro caso tenemos:


(5∙k) Instrucciones útiles
+ 4 Instrucciones de overhead
1
Porcentaje de saltos = ∙ 100%
(5∙k) + 4
Instrucciones
(5∙k) + 4 ⇒ 1000 4000
iteración Instrucciones ejecutadas = ∙ (5∙k) + 4 ⇒ ⏟ +
5000
k instrucc.
⏟ k
instrucc.
{ útiles
overhead

Conclusiones del Desenrollado del código de bucles:

 Se reducen el número de instrucciones de overhead.

 La reordenación de las instrucciones reduce el porcentaje de saltos al permitir insertar más instrucciones
entre dos saltos.

 Se necesitan más registros (típico en máquinas RISC) para realizar el desenrollado.

 Se necesita conocimiento de la endo-arquitectura de la máquina, tales como las latencias de las


Unidades Funcionales, de los bypasses, etc. lo que dificulta la compatibilidad software.

51
2.3.4 Software pipelined.

Un bucle software-pipelined intercala instrucciones de diferentes iteraciones sin desenrollar el bucle, por lo que
sólo es útil para bucles paralelizables. Es por tanto una alternativa al desenrollado de bucles cuando, bien
tenemos pocos registros (como ocurre en máquinas CISC) o bien, cuando nos preocupa el tamaño del código.

Esta técnica se basa en la segmentación del software con encadenamiento de iteraciones. El desenrollado es
eficaz, pero crece de manera significativa el tamaño del código estático, lo que suele aumentar el número de
fallos de caché, exigiendo contar para ello con muchos registros para resolver el renombrado.

Esquema de un bucle DAXPY paralelizable escrito de izquierda a derecha, donde hay muchas dependencias
reales. Dichas dependencias sólo han sido indicadas en la iteración 0, pero se repiten en el resto de iteraciones.
Prólogo (cálculo previo)
Iter 0 LDX0 LDY0 MULTD0 ADDD0 SD0 overhead

Iter 1 LDX1 LDY1 MULTD1 ADDD1 SD1 overhead

Iter 2 LDX2 LDY2 MULTD2 ADDD2 SD2 overhead

Iter 3 LDX3 LDY 3 3


MULTD ADDD 3 SD3 overhead

Iter 4 LDX 4 LDY 4 4


MULTD ADDD 4 SD4 overhead

Iter 5 LDX5 LDY5 MULTD5 ADDD5 SD5 overhead

Iter 6 LDX6 LDY6 MULTD6 ADDD6 SD6 overhead

Iter 7 LDX7 LDY7 MULTD7 ADDD7 SD7 overhead

Iter 8 LDX 8 LDY 8 MULTD ADDD8


8 SD8
Epílogo

Las columnas coloreadas y punteadas centrales se corresponden con las mismas operaciones útiles de cada una
de las distintas iteraciones. Se genera por tanto un nuevo bucle con operaciones independientes.

Luego el nuevo bucle será el formado por las siguientes instrucciones:


SD0 (R1)0, FS ; FS es requerido para la iteración siguiente,
ADDD1 FS, Fax, FY ; por lo que permanecerá ‘dormido’ hasta entonces.
MULTD2 Fax, Fa, FX
LDY3 FX, (R1)0
LDX4 FY, (R2)0
overhead

Si desenrollamos el bucle DAXPY dos líneas tendremos:


Iter 0 SO0
Iter 1 SD1
Aquí observamos un desenrollado más el software pipelined.
Iter 0 ADDD2
Iter 1 ADDD3 Es lo que se utiliza en VLIW, requiriendo muchas operaciones
Iter 0 MULTD4 independientes.
Iter 1 MULTD5
Iter 0 LDY6
Iter 1 LDY7
Iter 0 LDX8
Iter 1 LDX9
overhead

52
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Veamos, como en el caso anterior, un nuevo ejemplo a partir del siguiente código:
for (i=0 ; i<M ; i++ ) { y[i] = x[i] * s; }

Bucle_original:
; Instrucciones útiles
LD F2, 0(R1)
MULTD F4, F2, F24
SD (R3)0, F4
; Instrucciones de overhead
ADDI R1, R1, 8
ADDI R3, R3, 8
SLTI R7, R1, fin_array_x
BNEZ R7, Bucle_original

Esquema de ejecución del Software-pipelined, centrándonos únicamente en las instrucciones útiles:

Las flechas rojas (horizontales) se corresponden con una ejecución normal y las flechas verdes (verticales) con
software pipelined.

Veamos cómo las instrucciones con dependencias reales se separan en iteraciones diferentes:
Inicio:
LD0
MULTD0
LD1
Bucle_SW_PP:
SD0 (K)
MULTD1 (K+1)
LD2 (K+2)
; Instrucciones overhead (incluyendo un salto a Bucle_SW_PP)
Final:
SD M-2
MULTD M-1
SD M-1

Conclusiones del Software Pipelined:

 Las instrucciones de overhead deben modificar constantes correctamente, lo que no es una tarea fácil.
 Aparecen instrucciones adicionales de overhead tanto en el inicio como en el final.
 Al menos debemos ejecutar 2 iteraciones.
 Algunos compiladores avanzados pueden implementar estas técnicas, pero el Software pipelined es
difícil de implementar.
 El soporte Hardware es de mucha utilidad (por ejemplo IA-64).

53
2.3.5 Ejecución predictiva o condicional.

Los saltos especificados en el código son el mayor de los límites para la planificación estática. Algunos saltos son
fácilmente predecibles, por ejemplo, los bucles normalmente son tomados, y los saltos por overflow
normalmente no se toman. Sin embargo hay otros bucles que no son tan fáciles de predecir, por ejemplo:
for (i=0; i<n; i++) {
if (a[i] < 0) {
b[i] = -a[i];
} else {
b[i] = a[i];
}
}

Una solución sería extender el conjunto de instrucciones para incluir instrucciones predicadas o condicionales:

- Si la condición es cierta entonces la instrucción se ejecuta normalmente.


- Si la condición es falsa entonces la ejecución continua como si la instrucción fuera una de tipo NOOP.

Estos nuevos tipos de instrucciones pueden ser usados para eliminar los saltos, y convertir las dependencias de
control en dependencias de datos.

Las instrucciones predicadas o condicionales (Conditional MOVe [Not] Zero) se comportan como una instrucción
tipo if implementada en hardware, tenemos:

 CMOVZ Rd, Rf, Rcondición; equivalente a: if (Rcondición == 0) Rd ← Rf;


 CMOVNZ Rd, Rf, Rcondición; equivalente a: if (Rcondición != 0) Rd ← Rf;

Por tanto, el objetivo de la ejecución predictiva o condicional es eliminar la instrucción if ... else cuando:

 La condición es difícil de predecir.


 El cuerpo if ... else es pequeño.

Los microprocesadores MIPS, Alpha, PowerPC, SPARC, e Intel x86 soportan los juegos de instrucciones predictivas
o condicionales. Además, las arquitecturas ARM, el Intel VLIW Itanium IA-64 y muchos DSPs (Digital Signal
Processor) soportan la predicación completa para casi todas las instrucciones.

Ejemplo: k = min(c, m, y); //mínimo de 3 valores Traducido a bajo nivel usando instrucciones predictivas.

k = c; ADDI Rk , Rc, 0 ; k ← c ( k ← c+0)


if (k > m) k = m; SGT Rcond1, Rk, Rm ; si k > m entonces Rcon1 ← 1
if (k > y) k = y; CMOVNZ Rk , Rm, Rcond1 ; si Rcon1 = 1 entonces k ← m
SGT Rcond2, Rk, Ry ; si k > y entonces Rcon2 ← 1
CMOVNZ Rk , Ry, Rcond2 ; si Rcon2 = 1 entonces k ← y

54
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo:

Código fuente de alto nivel:


Rb = abs(Ra)
Rb = Ra;
if (Ra < 0) Rb = -Rb;

Código en bajo nivel:


ADDI Rb , Ra, 0 ; Rb ← Ra (Rb ← Ra+0)
SLTI Rcond, Rb, 0 ; si Rb < 0 entonces Rcond ← 1
BEQZ Rcond, sigue ; si Rcond = 0 entonces salta a sigue
SUB Rb , R0, Rb ; Rd ← -Rb (Rd ← 0-Rb) Sólo es ejecutada si no saltó
sigue: …

Como el salto es difícil de predecir entonces usamos CMOVZ:


ADDI Rb , Ra, 0 ; Rb ← Ra (Rb ← Ra+0)
SGEI Rcond, Rb, 0 ; si Rb ≥ 0 entonces Rcond ← 1
SUB Rd , R0, Rb ; Rd ← -Rb (Rd ← 0-Rb) Se ejecutada siempre!
CMOVZ Rb , Rd, Rcond ; si Rcond = 1 entonces Rb ← Rd

Vamos a utilizar otro conjunto de instrucciones predictivas, válidas para todas las instrucciones, las conocidas
como Predicación Universal (Full Predication):
[ Rc] Operador Rd, Rf1, Rf2
[!Rc] Operador Rd, Rf1, Rf2

Ejemplo:

Código fuente de alto nivel:


Rb = abs(Ra)
Rb = Ra;
if (Ra < 0) Rb = -Ra;

Código en bajo nivel:


MOV Rb , Ra ; Rb ← Ra
CMPLT Rcond, Rb, R0 ; si Rb < 0 entonces Rcond ← 1
BEQZ Rcond, sigue ; si Rcond = 0 entonces salta a sigue
SUB Rb , R0, Ra ; Rb ← -Ra (Rb ← 0-Ra)
sigue: …

Con la Predicación Universal quedaría como:


MOV Rb , Ra ; Rb ← Ra
CMPLT Rcond, Rb, R0 ; si Rb < 0 entonces Rcond ← 1
[Rcond] SUB Rb , R0, Ra ; Rb ← -Ra (Rb ← 0-Ra) SUB es ejecutado siempre
; pero el registro Rb es modificado sólo si Rcond = 1

Observamos que hemos predicado el cuerpo condicionado por el if , el registro Rcond. Esto es, sólo se ejecutará
si el registro Rcond vale 1 (es verdadero).

55
Ejemplo: ¿Qué ocurriría en un bloque condicional cuyo cuerpo es muy grande?
if (Rcond)
{
Oper1 [ Rcond] Oper1
Oper2 [ Rcond] Oper2
Oper3 [ Rcond] Oper3
. . .
}
else
{
Oper1 [!Rcond] Oper1
Oper2 [!Rcond] Oper2
Oper3 [!Rcond] Oper3
. . .
}

Es evidente que resulta un conjunto enorme de instrucciones independientes y reordenable sin saltos.
Todas las instrucciones se ejecutarán, independientemente del valor de Rcond. Solo que se llevarán a
efecto aquellas en las que se verifique que Rcond = 1 (es verdadera).

Ejemplo: Tenemos que tener en cuenta que esta técnica sólo es conveniente cuando el cuerpo del condicionante
tiene pocas instrucciones, en torno a 6 o 7 como máximo, ya que el código resultante obtenido ejecuta
siempre ambos conjuntos de instrucciones.

Supongamos el siguiente fragmento de código dentro de un bucle, los registros que utilizaremos:
aux = aux * 2; Raux
if (aux < v[j])
{ Rv que apunta a &v[j]
v[j] = 2 * v[j]
} else {
v[j] = 0;
w[j] = -w[j]; Rw que apunta a &w[j]
}

El código resultante en bajo nivel quedaría así:


MULT Raux, Raux, R2 ; aux = aux * R2 (R2 contiene un 2)
LW R7, (Rv) ; R7 ← v[j]
SLT Rcond, Raux, R7 ; si aux < R7 entonces Rcond ← 1
[ Rcond] MULT R7, R2, R7 ; si Rcond = 1 entonces R7 ← R7 * R2
Cuerpo del if
[ Rcond] SW (Rv), R7 ; si Rcond = 1 entonces almacena R7 en v[j]
[!Rcond] SW (Rv), R0 ; si Rcond = 0 entonces almacena 0 en v[j]
[!Rcond] LW R8, (Rw) Cuerpo del else
; si Rcond = 0 entonces R8 ← w[j]
[!Rcond] SUB R8, R0, R8 ; si Rcond = 0 entonces R8 ← - R8
[!Rcond] SW (Rw)0, R8 ; si Rcond = 0 entonces almacena R8 en w[j]

56
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.4 Ejercicios.

2.4.1 Problemas tipados.

Técnicas Estáticas.

1. Dibujar con flechas las dependencias RAW de la siguiente traza de ejecución (son dos iteraciones de un
bucle). Indicar cuales son dependencias ficticias (WAW y WAR):
LF F0,0(R1),
ADDF F4,F0,F2
SF 0(R1),F4,
SUBI R1,R1,4 // resta
BNEZ R1,BUCLE
LF F0,0(R1),
ADDF F4,F0,F2
SF 0(R1),F4,
SUBI R1,R1,4
BNEZ R1,BUCLE // salto condicional

2. ¿Cuál/cuáles de las siguientes dependencias se elimina por renombrado de registros?

RAW, WAR, WAW, Ninguna

3. Dibujar el camino crítico (cadena más larga de operaciones con dependencia real RAW). Suponemos que la
duración de toda operación es la misma.
LF F0,0(R1)
LF F2,0(R2)
MULTF F4,F0,F2
ADDF F4,F4,F2
ADDI R1, R1, 4
SUBI R2, R2, 4
SLT R4,R1,R2 // comparación
SF 0(R1),F4
BNEZ R4,BUCLE // salto condicional

4. Si se desenrollan tres iteraciones del bucle SAXPY ( variables float; cuerpo del bucle: y[i] += a * x[i] )
¿cuál sería el número de operaciones de memoria y de FP por iteración?

- 1 MULT FP, 3 ADD FP, 3 Accesos


- 1 MULT FP, 3 ADD FP, 6 Accesos
- 3 MULT FP, 3 ADD FP, 9 Accesos
- 3 MULT FP, 0 ADD FP, 3 Accesos
- 3 MULT FP, 0 ADD FP, 6 Accesos
- 3 MULT FP, 0 ADD FP, 9 Accesos

5. ¿Qué interesa más desenrollar el bucle interno o el externo del siguiente código?
for (f=0; f<m; f++)
for (c=0; c<m; c++)
a[f][c] = a[f][c] + b[f][c];

6. Desenrollar 3 iteraciones del bucle interno del anterior (en lenguaje de alto nivel).

7. Desenrollar 3 iteraciones del bucle (en lenguaje de alto nivel):


for (f=m; f>0; f=f-2)
a[f] = a[f] + b[f+4];

57
8. Dibujar el camino crítico del bucle desenrollado anterior.

9. Indique cómo se escriben en lenguaje C los siguientes bucles para que sean paralelizables, si se utilizan varios
acumuladores parciales dentro del bucle, los cuales después se suman tras el bucle. Traducir a ensamblador
el último bucle.
for (i=0; i<1024; i++)
p[i] = p[i] + x[i] * y[i];
for (i=0; i<1024; i++)
p = p + x[i] * y[i];

10. Desenrollar 4 veces el bucle que calcula el máximo de un vector, usando máximos parciales, donde cada
máximo parcial extrae el máximo de elementos contiguos del vector (una cuarta parte):
max=a[0];
for (f=1; f<m; f++)
if (max<a[f])
max = a[f];

11. ¿Qué cantidad de cargas de memoria y de sumadores en paralelo se necesitan para poder ejecutar este bucle
en forma de árbol binario (utilizando la reasociación de sumas)?
for (f=0; f<m; f++) { suma += (a[f] * b[f]); }

12. Indique cuáles de los siguientes bucles son paralelizables directamente (traduciendo a ensamblador
directamente), y cuáles podrían paralelizarse con cambios en el código de alto nivel).
for (i=0; i<1024; i++)
x[i] = x[i] * y[i];
for (i=0; i<1024; i++)
x[i+1] = x[i] * y[i];
for (i=0; i<1024; i++)
x[i-1] = x[i] * y[i];
for (i=0; i<1024; i++)
p[i] = p[i] + x[i] * y[i];
for (i=0; i<1024; i++)
p = p + x[i] * y[i];

13. Buscar en internet un bucle que resuelva una ecuación por un método iterativo, ¿su bucle será paralelizable
o no? Dibujar su camino crítico.

14. Dibujar en un grafo el camino crítico del cálculo de la serie de Fibonacci.

15. Dibujar en un grafo el camino crítico de una sola iteración del siguiente bucle. Después dibujar un esquema
de cómo se aplicaría el método del software pipeline.
for (i=0; i<1024; i++)
p[i] = (x[i] * y[i]) + (z[i] * w[i]);

16. Pensar en qué valores deben tener c1 y c2, para que los tres siguientes bucles hagan la misma operación.
Traducir el primero de ellos a ensamblador usando instrucciones con predicación (Full predication)
for (i=0; i<1024; i++)
if (cond[i]==0) p[i] = x[i] / y[i];
else p[i] = z[i];
for (i=0; i<1024; i++)
p[i] = (x[i] / y[i]) * c1[i] + (z[i] * (!c1[i]));
for (i=0; i<1024; i++)
p[i] = (x[i] / y[i]) & c2[i] | (z[i] & (~c2[i]));

58
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Técnicas dinámicas.

1. Poner dos ejemplos en donde una BTB con correlación consiga menor porcentaje de fallos que una BTB que
sólo guarda información local de cada salto.

2. Suponiendo que la predicción de una BTB es siempre T para todo salto, hallar el porcentaje de fallos para un
bucle anidado de 10 iteraciones externas y 3 internas.

3. Renombrar los registros del siguiente código:


BUCLE:
LD F0, (R1)0
MULTD F0, F0, F2
ADDI R1, R1, 8
CMPLT R2, R1, RFIN
BNEZ R2, BUCLE

4. Hallar el camino crítico para una única iteración del anterior bucle.

5. Hallar el camino crítico para muchas iteraciones del anterior bucle.

6. Esbozar el cronograma del Algoritmo de Tomasulo (fases IS E1 para todas las instrucciones, excepto para
MULT que es IS E1 E2 E3) para una única iteración del anterior bucle. Suponer que se emiten 5 instrucciones
por ciclo.

7. Ídem para muchas iteraciones del anterior bucle.

8. Dibujar un cronograma de 6 instrucciones con RAW para un procesador que emiten 3 instrucciones por ciclo
donde las etapas son IS E1 E2.

9. Sea un programa que sólo contiene instrucciones enteras (etapas son IS E1) entre las cuales siempre existe
una dependencia real entre cada pareja de instrucciones consecutivas. Para el Algoritmo de Tomasulo clásico
con estaciones de reserva y que emiten 3 instrucciones por ciclo, ¿existirá bloqueo estructural por
agotamiento de RS? Calcule el CPI tras muchas instrucciones (en el estacionario).

10. Sea un bucle que contiene 1 instrucción de DIV por iteración (UF no segmentada que dura 30 ciclos). La
duración de cada iteración, ¿crecerá con el número de instrucciones de overhead del bucle?

11. Un procesador tiene 1 UF de MULT segmentada, y suponemos que ejecuta un código sólo con
multiplicaciones (que no tiene dependencia entre sí). ¿Será el CPI función de la duración de la UF de MULT?

12. Un procesador tiene 4 UF de MULT segmentadas, y suponemos que ejecuta un código sólo con
multiplicaciones (que no tiene dependencia entre sí). ¿Será el CPI función de la duración de la UF de MULT?
¿Cuál será la aceleración respecto del caso anterior?

13. Sea un bucle de 5 instrucciones que ejecuta K iteraciones. Suponiendo que se emiten 5 instrucciones/ciclo,
comparar la duración entre los casos: BTB acierta siempre, BTB falla siempre, BTB sólo falla en la última
iteración. Suponer una latencia del salto de 10 ciclos.

14. ¿Cuáles son las causas posibles de que se agoten las R.S. y que por tanto haya bloqueos de emisión?

15. Para cada causa, poner un ejemplo de un código en lenguaje de alto nivel, donde haya al menos una
instrucción que cumpla tal causa.

16. Supongamos que toda Unidad Funcional dura 1 ciclo y que se emiten 3 instrucciones/ciclo y que la BTB
siempre acierta en la predicción. Escribir un cronograma simplificado, donde se señalen las instrucciones de
la segunda iteración que necesitan que exista un ROB, porque modifican el estado del procesador (escriben
en registro o memoria).

59
17. Para el siguiente cronograma, pensar cómo habría que añadir la fase CM (con 4 instrucciones por ciclo). Las
fases E pueden tener escrituras en registro o memoria (WB o MEM).
IS E1 E2 E3
IS E1 E2 E3
IS E1
IS E1
IS E1
IS E1 E2 E3
IS E1
IS E1 E2 E3

18. Para el código dado, suponiendo que la caché y la BTB siempre acierta, calcular el número de ciclos por
iteración, sabiendo que se pueden dar estos 3 cuellos de botella. Suponer 3 instrucciones de overhead
(enteras) por iteración. ¿Cuál es el cuello de botella principal?

a) m = 5 (grado superescalaridad).
b) Sólo 2 Unidades Funcionales INT.
c) Sólo 2 puertos de caché.
double x[N], y[N], z[N], w[N], y1[N], y2[N];
for (i=0; i<3000; i++) {
y1[i] = x[i] * y[i];
y2[i] = w[i] + z[i];
}

19. Ídem que el anterior si el código se desenrolla 6 iteraciones previamente. ¿Acelera la ejecución el
desenrollado?

20. Sea un VLIW puro con p = 5 operaciones por macro-instrucción. Para el bucle SAXPY de 1000 iteraciones,
escribir como quedarían las macro-instrucciones y cuál sería su duración en ciclos, si se desenrollan 5
iteraciones y se intenta reducir el número de macro-instrucciones. Duración ADDF y MULTF 2 ciclos, resto 1
ciclo. Sólo 2 puertos de caché.

21. Sea el bucle SAXPY de 5 instrucciones útiles y 3 de overhead. Se van a estudiar el código ensamblador escalar
y el código vectorizado con registros vectoriales (multimedia) de 128 bits (sufijo ps). Hallar los GFLOPS de
ambos (frec = 3 GHz) y la aceleración A entre ambos para estos 3 casos, si m = 4 (grado superescalaridad).

a) Las cachés nunca fallan.


b) La caché falla 1 de cada 48 accesos (float) y hay una penalidad de 20 ciclos por fallo.
c) Las cachés fallan mucho, de forma que los accesos a RAM (ABRAM de 4 GB/s) son el cuello de botella.

22. Ídem que el anterior para el siguiente bucle y su versión vectorizada.


//todo vector es float
for (i=0; i<2048; i++)
acc = acc + x[i]; // Duración ADDF 3 ciclos
for (i=0; i<2048; i++)
vacc[0:3] = vacc[0:3] + x[i:i+3]; // Duración ADDF 3 ciclos. Se vectoriza con ps
acc = vacc[0] * vacc[1] * vacc[2] * vacc[3];// el tiempo de ejec. de esta línea es despreciable

60
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

ILP.

1. ¿Cuál es el CPI ideal de un VLIW con 5 operaciones por instrucción y que además es superescalar de grado
m = 3?

2. ¿Cuál es el CPI ideal de un superescalar de grado m = 4, que además es superencadenado y tiene 15 etapas
por instrucción?

3. Un programa RISC tiene N instrucciones de 32 bits y se ejecuta con CPI medio de 0’5. Si el compilador es
capaz de compactar una media de 5 instrucciones en cada macro-instrucción de 256 bits (p=8) para un VLIW
puro, ¿cuál es la aceleración entre el RISC y el VLIW? Suponer igual frecuencia y que nunca fallan las cachés.

4. ¿Cuál sería la relación de tamaño de código entre el RISC y el VLIW del caso anterior?

5. Calcular el IPC real de un superescalar de grado 4, que tiene CPI bloqueo = 0’5

6. Hallar la relación de CPI entre caso ideal y real para un superescalar de grado m, con CPI bloqueo = c

7. Si el grado de superescalaridad de un procesador es m pero solo ejecuta m-1 instrucción por ciclo, sería
correcta esta afirmación: “Podemos hablar de un IPC bloqueo de 1 instrucciones/ciclo, de manera que el
IPC real = m – IPC bloqueo”.

8. Si el CPI bloqueo de una máquina es 0’5, y su IPC ideal es 3, entonces el IPC real es: IPC ideal – 2 = 1, puesto que el
IPC bloqueo es la inversa del CPI bloqueo (1/0’5) ¿es correcto este cálculo?

9. A pesar del ejercicio anterior, recuerde que los cálculos deben hacerse con el CPI (puesto que siempre se
pueden sumar diferentes CPI, pero casi nunca se pueden sumar los IPC). Calcular entonces el CPI bloqueo
del ejercicio anterior.

10. ¿Cuál es el número de macroinstrucciones en vuelo para un RISC (planificación estática) de grado m de
superescalaridad, 15 etapas, y que es VLIW con p operaciones por macroinstruccion? ¿y el número de
operaciones en vuelo?

11. Un procesador con emisión estática significa que:

- El compilador prepara las instrucciones para que las emita el procesador sin comprobar nada.
- El procesador no emite las instrucciones nunca.
- El procesador prepara las instrucciones reordenándolas antes de emitirlas.
- Ninguna de las otras es correcta.

12. Según lo anterior, un procesador con emisión dinámica significa que:

- El compilador prepara las instrucciones para que las emita el procesador sin comprobar nada.
- El procesador no emite las instrucciones nunca.
- El procesador prepara las instrucciones reordenándolas antes de emitirlas.
- Ninguna de las otras es correcta.

13. Para un DLX con Algoritmo de Tomasulo, especulación dinámica y fases IF IS EX WB, qué características tiene:

- In-order-execution.
- Out-of-order-execution.
- Static issue.
- Dynamic issue.

61
14. Para un DLX con Algoritmo de Tomasulo y fases IF IS EX WB, qué frase/s son verdad para su BTB:

- Se predice un salto en la fase IF y se resuelve en EX.


- Se busca la instrucción predicha un ciclo después de IF.
- Se busca la instrucción predicha en la fase EX.
- Se predice un salto en la fase IS, se busca la instrucción predicha en tal ciclo y se resuelve en EX.
- Se predice un salto en la fase IF, y se resuelve el salto también en IF.

15. Clasificar los tipos de procesadores con ILP según sean la planificación y la emisión estáticas o dinámicas.

16. Ordenar de mayor a menor consumo de potencia, los procesadores: VLIW, superescalar estático,
superescalar dinámico.

17. ¿Por qué no es cierto que un superencadenado con frecuencia 3 veces mayor que el procesador que no lo
es, sea 3 veces más rápido?

DLP.

1. ¿Cuál es la aceleración ideal para registros multimedia de 256 bits, si se usan variables float? ¿Y double?

2. Un bucle vectorizado ejecuta 4 veces menos iteraciones que su versión escalar, ¿ejecuta también 4 veces
menos accesos a memoria? ¿y consume también 4 veces menos ancho de banda ABRAM?

3. ¿Cuál es la zancada al recorrer una matriz de 20 x 20 elementos por columnas?

4. Cuál de estas dos versiones interesa vectorizar:


for (i=0; i<256; i++)
for (j=0; j<256; j++)
a[i][j] = 0.0;

for (j=0; j<256; j++)


for (i=0; i<256; i++)
a[i][j] = 0.0;

62
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.4.2 Problemas de clase.

P1. Sea el bucle SAXPY de 5 instrucciones útiles y 3 de overhead. Se van a estudiar el código ensamblador escalar
y el código vectorizado con registros vectoriales (multimedia) de 128 bits (sufijo ps). Hallar los GFLOPS de ambos
(frecuencia = 3 GHz) y la aceleración A entre ambos para estos 3 casos, si m = 4 (grado superescalaridad).

a) Las cachés nunca fallan.

b) La caché falla 1 de cada 48 accesos (float) y hay una penalidad de 20 ciclos de bloqueo por fallo.

c) Las cachés fallan mucho, de forma que los accesos a RAM (ABRAM de 4 GB/s) son el cuello de botella.

Solución:

El bucle escalar SAXPY viene definido por las siguientes instrucciones de bajo nivel:

SAXPY Escalar SAXPY Vectorial


LFx MOVAps
LFy MOVAps
MULTF MULTps
ADDF ADDps
SF MOVAps
+3 Inst. overhead +3 Inst. overhead

Apartado a) Las cachés nunca fallan.

Como el bucle es paralelizable, el tiempo lo dará el CPI ideal donde:


(5+3) instrucciones 1 ciclo ciclos
CPIIdeal = ∙ = 2
1 iteración m=4 instrucciones iteración
Pero,
2 ciclos 2 ciclos ciclos
Escalar: → =2
1 iteración 1 elemento procesado Elto. Procesado

2 ciclos 2 ciclos ciclos


Vectorial: → = 0'5
1 iteración 4 elementos procesados Elto. Procesado
El caso vectorial resulta ser 4 veces más rápido, A = 4. Esto se debe a que en el caso vectorial
se ejecuta 4 veces menos iteraciones, pues cada iteración contiene 4 elementos de x[] y[].

Cálculo de los GFLOPS [Hemos invertido todos los conceptos]:


1 iteración 3 Gciclos 2 FLOPS GFLOPS
GFLOPSEscalar : ∙ ∙ = 3
2 ciclos 1 segundo 1 iteración segundo

1 iteración 3 Gciclos (2 ∙ (m=4) = 8) FLOPS GFLOPS


GFLOPSVectorial : ∙ ∙ = 12
2 ciclos 1 segundo 1 iteración segundo

63
Apartado b) La caché falla 1 de cada 48 accesos (float) y hay una penalidad de 20 ciclos de bloqueo por fallo.

Acc 1º 2º 3º … … 48º 49º …


X X

Falla Acierta Vuelve a fallar


(20 ciclos añadidos) (ejecución ideal)

16 iteraciones
Escalar ≡ [
1 iteración tiene 3 accesos
El “patrón” de ejecución para los 48 accesos sería
4 iteraciones
Vectorial ≡ [
{ 1 iteración tiene 12 accesos

Luego, la relación será:


16 iteraciones 16 elementos procesados
Escalar: =
2∙16 ciclosideales + 20 ciclospenalidad 52 ciclos

4 iteraciones 16 elementos procesados


Vectorial: =
2∙4 ciclosideales + 20 ciclospenalidad 28 ciclos
52
La aceleración será de A = . Observamos que se va “degradando” según empieza a fallar las cachés.
28
En lugar de A = 4 , ahora tenemos: A = 1’86.

Cálculo de los GFLOPS:


3 Gciclos 2 ∙16 FLOPS 96 GFLOPS
GFLOPSEscalar : ∙ = = 1'85
1 segundo 52 ciclos 52 segundos

3 Gciclos 2 ∙16 FLOPS 96 GFLOPS


GFLOPSVectorial : ∙ = = 3'43
1 segundo 28 ciclos 28 segundos

Apartado c) Las cachés fallan mucho, de forma que los accesos a RAM (ABRAM de 4 GB/s) son el cuello de botella.

La degradación será total cuando el acceso a RAM sea el cuello de botella. Partiendo de tal cuello
tenemos, tanto para escalar como para vectorial:
Estas dos relaciones son inherentes al SAXPY
tanto Vectorial como Escalar
⏞ 𝑀𝑈𝐿𝑇𝐷,𝐴𝐷𝐷𝐷
ABRAM
( ⏞
2 ) FLOP
⏞4 GBytes 1 elto. procesado GFLOPS
GFLOPS = ∙ ∙ = 0'6̂
1 segundo 1 elto. procesado segundo
( ⏟
3 ) ∙ 4 Bytes
𝐿𝐹,𝐿𝐹,𝑆𝐹

Y por supuesto, A = 1 en este caso apartado c.

64
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P2. Se quiere calcular la suma de cuadrados de un conjunto de valores: ∑𝑁−1 2


𝑖=0 𝑋𝑖 de tipo double. Si se dispone
de un procesador de 2 GHz y con latencia (duración EX + WB) de: ADDD = 4, MULTD = 5, LD = 3, resto = 1, y con
superescalaridad m = 3,

a) Hallar el número de ciclos por iteración y de GFLOPs para:


a.1) Caso ideal (sin bloqueos).
a.2) Caso real si acierta en caché siempre.

b) Hallar el ancho de banda de acceso a la RAM (AB RAM) mínimo para que no suponga cuello de botella.

c) Si se desenrollaran 4 iteraciones con acumuladores parciales, ¿Cuáles serían los valores para los
apartados anteriores a.2 y b?

Solución:

Antes de empezar observemos cómo sería el código:


1 LD LD LD LD
2 MULTD Dependencias En el estacionario, el camino
3 ADDD reales crítico es la ADDD.
* * *
-----
4 ADDI +
5 CMP
6 BNEZ ADDI
+
CMP
+
BNEZ

Apartado a) Hallar el número de ciclos por iteración y de GFLOPs.


6 instrucciones 1 ciclo 2 ciclos
a.1) CPICaso.Ideal : ∙ =
⏟ 1 iteración ⏟
3 instrucciones 1 iteración
Nº instrucciones m=3

2 Gciclos 2 FLOPs 1 iteración GFLOPS


GFLOPSCaso.Ideal : ∙ ∙ = 2

1 segundo 1 ⏟iteración ⏟2 ciclos segundo
ABRAM Nº Instc. float 1⁄
CPI

2 Gciclos 4 ciclos ADDD 1 iteración GFLOPS


a.2) GFLOPSCaso.Real ∶ ∙ ∙ = 4
1 segundo ⏟1 iteración
⏟ ⏟2 ciclos segundo
ABRAM Nº Instc. float 1⁄
CPI

Apartado b) Hallar el ancho de banda de acceso a la RAM (AB RAM) mínimo para que no suponga cuello de botella.
1 iteración 1 LD GBytes 1 segundo ≥ 1 iteración
∙ ∙ AB ∙
⏟ 1 LD 8 Bytes segundo 2 Gciclos ⏟ ⏟4 ciclos
Cuello de botella del acceso a RAM debe ser más rápido caso a.2

Por tanto, se despeja AB (que está expresado en GBytes/segundo) y se obtiene:


GBytes
AB ≥ 4
segundo
65
Apartado c) Si se desenrollaran 4 iteraciones con acumuladores parciales, ¿Cuáles serían los valores para los
apartados anteriores a.2 y b?

Si se desenrolla con acumuladores parciales tendremos el siguiente código:


for (i=0; i<N; i+=4)
{
a0 += X[i] * X[i];
a1 += X[i+1] * X[i+1];
a2 += X[i+2] * X[i+2];
a3 += X[i+3] * X[i+3];
}
a = a0 + a1 + a2 + a3;

Para el apartado a.2)

Ahora el nuevo camino crítico es el de cualquier acumulador, pero en una iteración se hacen 4
sumas (4 elementos de X[i]), luego el número de ciclos por iteración no cambia, sigue siendo:
4 ciclos
1 iteración de 4 ADDD

Sin embargo, la velocidad es 4 veces mayor, esto es, 4 GFLOPs.

Para el apartado b)

Se consume en todos 4 ciclos, esto es, 4 accesos a datos de tipo Double, luego:
1 iteración (desenrollada) 1 LD GBytes 1 segundo 1 iteración (desenrollada)
∙ ∙ AB ∙ ≥
4 LD 8 Bytes segundo 2 Gciclos 4 ciclos
Por tanto, se despeja AB (que está expresado en GBytes/segundo) y se obtiene:
GBytes
AB ≥ 16
segundo

66
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.4.3 Problemas de bucles.

Se han elegido aleatoriamente bucles del libro Numerical Recipes in C: The Art of Scientific Computing. Second
Edition, William H. Press, Saul A. Teukolsky, William T. Vetterling, Brian P. Flannery.

Para los siguientes ejercicios se pide dibujar el camino crítico.

Pistas:

- Los ejercicios 1 y 3 son bucles paralelizables fáciles.

- El ejercicio 2 tiene dependencia WAR en memoria d[k-1] con respecto a d[k], pero es paralelizable.

- El ejercicio 4 tiene la notación de C de puntero que se incrementa sp1 += 2 para cambiar de elementos,
pero es también paralelizable.

- El ejercicio 5 tiene un for con if() dentro, por lo que necesitaríamos estudiar instrucciones predicadas.

Ejercicio 1. Sistemas lineales dispersos (Sparse Linear Systems).


for (j=1;j<=n; j++) {
r[j] = b[j] - r[j];
rr[j] = r[j];
}

Ejercicio 2. Aproximación polinómica de los coeficientes de Chebyshev (Polynomial Approximation from


Chebyshev Coefficients).
for (j=n-2; j>=1; j--) {
for (k=n-j; k>=1; k--) {
sv = d[k];
d[k] = 2.0 * d[k-1] - dd[k];
dd[k] = sv;
}
}

Ejercicio 3. Funciones de minimación y maximación (Minimization or Maximization of Functions).


// The BFGS updating formula:
for (i=1; i<=n; i++) {
for (j=i; j<=n; j++) {
hessin[i][j] += fac*xi[i]*xi[j] - fad*hdg[i]*hdg[j] + fae*dg[i]*dg[j];
hessin[j][i] = hessin[i][j];
}
}

67
Ejercicio 4. Transformadas de Fourier de datos reales en dos y tres dimensiones (Fourier Transforms of Real Data
in Two and Three Dimensions).
// Note how this can be made a single for-loop
// in- stead of three nested ones by using the pointers sp1 and sp2.
for (j=1; j<=N*N*N/2; j++) {
r = sp1[0]*sp2[0] - sp1[1]*sp2[1];
i = sp1[0]*sp2[1] + sp1[1]*sp2[0];
sp1[0] = fac * r;
sp1[1] = fac * i;
sp1 += 2;
sp2 += 2;
}

Ejercicio 5. Cuasi (es decir, Sub) secuencias aleatorias (Quasi- (that is, Sub-) Random Sequences).
for (k=1; k<=MAXDIM; k++) {
//Stored values only require normalization.
for (j=1; j<=mdeg[k]; j++)
iu[j][k] <<= (MAXBIT-j);
// Use the recurrence to get other values.
for (j=mdeg[k]+1; j<=MAXBIT; j++) {
ipp = ip[k];
i = iu[j-mdeg[k]][k];
i ^= (i >> mdeg[k]);
for (l=mdeg[k]-1; l>=1; l--) {
if (ipp & 1) i ^= iu[j-l][k];
ipp >>= 1;
}
iu[j][k] = i;
}
}
}

68
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

3 Paralelismo a Nivel de Hilos y Tareas.

3.1 Introducción.
Los arquitectos de computadores han buscado durante mucho tiempo El Dorado del diseño de los
computadores: crear sistemas potentes simplemente conectando muchos computadores más pequeños y
resistentes. Esta visión tomada es el origen de los multiprocesadores. Los clientes encargan tantos procesadores
como puedan permitirse y reciben un computador con prestaciones proporcionales al número de procesadores.
Para ello, el software de los multiprocesadores debe diseñarse para funcionar correctamente con un número
variable de procesadores. Como ya sabemos, el consumo de potencia se ha convertido en el principal enemigo
de los microprocesadores y por tanto de los centros de datos. Se pueden obtener mejores prestaciones por vatio
y/o por julio reemplazando los procesadores grandes e ineficientes por muchos procesadores más pequeños y
eficientes, siempre y cuando el software sea capaz de aprovecharlos al máximo. Así, en los multiprocesadores se
unen la mejora de la eficiencia energética y la escalabilidad de las prestaciones.

Como el software de los multiprocesadores debe ser escalable, algunos diseños mantienen su operatividad aún
en presencia de fallos en el hardware, es decir, si en un multiprocesador con n procesadores falla uno, el sistema
podría continuar funcionando correctamente con n - 1 procesadores, y así mejorar la disponibilidad.

El término “altas prestaciones” puede significar alta productividad de trabajos independientes, lo que se llama
paralelismo a nivel de trabajos o paralelismo a nivel de procesos. Estos trabajos paralelos son aplicaciones
independientes y actualmente constituyen una carga de trabajo importante y habitual de los computadores
paralelos. Utilizaremos el término programa de procesamiento paralelo para referirnos a un programa que se
ejecuta en varios procesadores simultáneamente.

En las últimas décadas ha habido grandes problemas científicos que han necesitado computadores mucho más
rápidos que los disponibles en cada momento, y estos problemas han servido para justificar muchos diseños de
computadores paralelos. Algunos de estos problemas pueden ser resueltos con un clúster formado por
microprocesadores alojados en servidores o PCs independientes. Además, los clústeres son útiles también para
otras aplicaciones no científicas, tales como motores de búsqueda, servidores web y bases de datos.

El problema del consumo de potencia han propiciado que los multiprocesadores sean el centro de atención, y
los futuros aumentos de las prestaciones aparentemente pasan por poner más procesadores en cada chip, en
lugar de disponer de mayores frecuencias de reloj o mejores CPI. Estos sistemas se llaman microprocesadores
multinúcleo en lugar de microprocesadores multiprocesador, probablemente para evitar nombres redundantes.
En consecuencia, los procesadores en un chip multinúcleo se llaman simplemente núcleos. La tendencia actual
es duplicar el número de núcleos cada dos años y, por tanto, los programadores tienen que preocuparse del
rendimiento o reconvertirse en programadores paralelos, ya que programa secuencial equivale a programa lento.

El principal reto que debe afrontar la industria de los computadores es crear hardware y software para facilitar
la escritura de programas de procesamiento paralelo, que se ejecuten eficientemente en términos de
prestaciones y consumo de potencia a medida que el número de núcleos escala geométricamente.

Este giro repentino en el diseño de los microprocesadores ha pillado desprevenido a muchos, por lo que hay
mucha confusión en la terminología y en lo que está significa. En la siguiente figura se intenta clarificar los
términos serie, paralelo, secuencial y concurrente. Las columnas representan el software que es inherentemente
secuencial o concurrente, mientras que las filas representan el hardware que es serie o paralelo. Por ejemplo,
los programadores de compiladores piensan que este como un proceso secuencial: los pasos son análisis léxico,
análisis sintáctico, generación de código, optimización, etc. Por el contrario, los programadores de sistemas
operativos piensan en ellos como un conjunto de programas concurrentes: procesos cooperantes que procesan
sucesos de E/S de trabajos independientes que se ejecutan en un computador.

69
Software
Secuencial Concurrente
Multiplicación de matrices en MatLab Sistema Operativo Windows Seven
Serie
ejecutándose en un Intel Pentium 4. ejecutándose en un Intel Pentium 4.
Hardware
Multiplicación de matrices en MatLab Sistema Operativo Windows Seven
Paralelo
ejecutándose en un Intel Xeon e5345. ejecutándose en un Intel Xeon e5345.
Categorización de hardware y software y ejemplos del punto de vista de la aplicación sobre concurrencia y el punto de vista del
hardware sobre paralelismo.

En la tabla anterior podemos destacar que el software concurrente puede ejecutarse en un hardware serie, como
el sistema operativo en el miocroprocesador Intel Pentium 4, o en un hardware paralelo, como el sistema
operativo en el procesador Intel Xeon e5345 (Clovertown). Lo mismo podemos decir de un software secuencial.
Por ejemplo, el programador de MatLab recibe una multiplicación de matrices pensando secuencialmente, pero
la puede ejecutar en serie en el Pentium 4, o en paralelo, en el Xeon e5345. Se podría pensar que el único reto
de la revolución paralela es averiguar cómo conseguir que el software secuencial tenga altas prestaciones en un
hardware paralelo, pero además hay que conseguir que los programas concurrentes tengan altas prestaciones
cuando se ejecutan en un multiprocesador y que éstas aumenten con el número de procesadores. Teniendo en
mente esta distinción, hablaremos de programa de procesamiento paralelo o software paralelo para referirnos
a software secuencial o concurrente que se ejecuta en un hardware paralelo.

La dificultad de crear programas de procesamiento paralelo.

La dificultad del paralelismo no está en el hardware; la principal dificultad es que muy pocas aplicaciones se han
reescrito para completar sus tareas en menos tiempo en un multiprocesador. Es difícil escribir software que
utilice varios procesadores para terminar una tarea de forma más rápida, y el problema empeora al aumentar el
número de procesadores.

¿Por qué esto es así? ¿Por qué es más difícil desarrollar programas de procesamiento paralelo que programas
secuenciales? La razón principal es que las prestaciones y la eficiencia de un programa de procesamiento paralelo
en un multiprocesador tienen que ser mejores; en caso contrario es preferible utilizar un programa secuencial
en un monoprocesador, ya que la programación es más sencilla. De hecho, las técnicas de diseño de
monoprocesadores, tales como superescalar y ejecución fuera de orden, aprovechan el paralelismo a nivel de
instrucción, normalmente sin la participación del programador. Estas técnicas disminuyen la demanda de
reescritura de programas para multiprocesadores, porque sin que los programadores tengan que hacer nada
más, sus programas secuenciales se ejecutarán más rápido en los nuevos computadores.

¿Por qué es difícil escribir programas de procesamiento paralelo que sean rápidos, especialmente cuando
aumenta el número de procesadores? Usando la analogía de ocho periodistas intentando escribir un artículo con
la esperanza de hacer el trabajo ocho veces más rápido, la tarea debe dividirse en ocho partes de la misma
complejidad, porque en caso contrario algunos periodistas estarían sin hacer nada, esperando a que terminase
en los que tienen las partes más complejas. Otro riesgo para las prestaciones se produciría cuando los periodistas
tienen que emplear demasiado tiempo en comunicarse con sus compañeros en lugar de trabajar en su parte del
artículo. Podemos extraer algunas conclusiones de esta analogía con la programación paralela, los desafíos
incluyen la planificación de las tareas, el equilibrio de la carga, el tiempo de sincronización y la sobrecarga de las
comunicaciones entre colaboradores. El reto es todavía más difícil cuantos más periodistas participen en la
escritura del artículo y cuantos más procesadores estén disponibles para la programación paralela. Aún existe
otro obstáculo, la Ley de Amdahl, que nos recuerda que incluso las partes pequeñas de un programa deben
paralelizarse si se quiere hacer un buen uso de los muchos núcleos disponibles. Aun así hay aplicaciones con
abundante paralelismo.

70
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo: El reto de la aceleración.

Suponga que queremos que un programa se ejecute 90 veces más rápido con 100 procesadores. ¿Qué
parte del programa original puede ser secuencial?

Por la Ley de Amdahl sabemos que:


Tiempo de ejecución afectado por la mejora
Tiempo de ejecución después de la mejora = + Tiempo de ejecución no afectado
Total de la mejora

Se puede reformular la Ley de Amdahl en términos de aceleración frente al tiempo de ejecución de la


versión original:

Tiempo de ejecución antes


Aceleración =
Tiempo de Tiempo de
Tiempo de ejecución afectado
( ejecución - ejecución ) +
100
antes afectado

Esta ecuación se reescribe considerando que el tiempo de ejecución antes es 1 para alguna unidad de
tiempo y expresando el tiempo de ejecución afectado por la mejora como un porcentaje del tiempo de
ejecución original:
1
Aceleración =
Tiempo de ejecución afectado
(1 - Porcentaje tiempo afectado) +
100

Sustituyendo nuestro objetivo, aceleración igual a 90, en la anterior ecuación, tenemos:


1
90 =
Tiempo de ejecución afectado
(1 - Porcentaje tiempo afectado) +
100

Simplificando y despejando el porcentaje de tiempo afectado:

90 x (1 - 0’99 x Porcentaje de tiempo afectado) = 1

90 - (90 x 0’99 x Porcentaje de tiempo afectado) = 1

90 - 1 = 90 x 0’99 x Porcentaje de tiempo afectado

Porcentaje de tiempo afectado = 89 / 89.1 = 0’999

Es decir, para obtener una aceleración igual a 90 con 100 procesadores, el porcentaje secuencial debe
ser sólo del 0’1%

71
Ejemplo: El reto de la aceleración → El mayor problema.

Suponga que queremos hacer dos sumas: una suma de 10 variables escalares y una suma matricial de
dos matrices bidimensionales de 10 x 10. ¿Qué aceleración se obtendrá con 10 y 100 procesadores?
Ahora calcule la aceleración suponiendo que las matrices son de 100 x 100.

Si aceptamos que las prestaciones son función del tiempo de suma, t, entonces hay 10 sumas que no se
benefician de la utilización de procesadores paralelos y 100 que sí. Si el tiempo en un procesador es 110t,
el tiempo de ejecución con 10 procesadores es:
Tiempo de ejecución afectado por la mejora
Tiempo de ejecución después de la mejora = + Tiempo de ejecución no afectado
Total de la mejora

100t
Tiempo de ejecución afectado de la mejora = + 20t = 20t
10

Entonces la aceleración con 10 procesadores es 110t / 20t = 5’5. El tiempo de ejecución con 100
procesadores es:

100t
Tiempo de ejecución después de la mejora = + 10t = 11t
100

Entonces la aceleración con 100 procesadores es 110t / 11t = 10.

Así, para este tamaño de problema, se obtiene el 55% de la máxima aceleración posible con 10
procesadores, pero sólo el 10% con 100.

Veamos qué ocurre cuando aumentamos las dimensiones de la matriz. El tiempo de ejecución del
programa secuencial ahora es 10t + 10000t = 10010t. El tiempo de ejecución con 10 procesadores es:

10000t
Tiempo de ejecución después de la mejora = + 10t = 1010t
10

Entonces la aceleración con 10 procesadores es 10010t / 1010t = 9’9. El tiempo de ejecución con 100
procesadores es:

10000t
Tiempo de ejecución después de la mejora = + 10t = 110t
100

Entonces la aceleración con 100 procesadores es 10010t / 110t = 91. Así, para un tamaño de problema
mayor, obtenemos aproximadamente el 99% de la máxima aceleración posible con 10 procesadores, y
más del 90% con 100.

72
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo: El reto de la aceleración → El equilibrio de la carga.

En el ejemplo anterior, para obtener una aceleración de 91 con 100 procesadores, se supuso que la carga
estaba perfectamente equilibrada. Es decir, cada procesador hacía el 1% del trabajo total. Muestre qué
impacto tiene sobre la aceleración que un procesador tenga más carga que el resto. Calcule con el 2% y el 5%.

Si un procesador tiene el 2% de la carga, entonces tiene que hacer 2% x 10000 = 200 sumas, y los otros
99 procesadores se reparten las 9800 sumas restantes. Como están trabajando simultáneamente,
podemos calcular el tiempo de ejecución como un máximo.
9800t 200t
Tiempo de ejecución después de la mejora = max ( , ) + 10t = 210t
99 1

La aceleración cae hasta 10101t / 210t = 48. Si un procesador tiene el 5% de la carga, debe hacer 500
sumas:
9500t 500t
Tiempo de ejecución después de la mejora = max ( , ) + 10t = 510t
99 1

La aceleración cae todavía más, hasta 10101t / 510t = 20.

Este ejemplo pone de manifiesto la importancia del equilibrio de la carga; un procesador con el doble de
carga que el resto hace caer la aceleración casi a la mitad, y con una carga 5 veces la del resto reduce la
aceleración casi en un factor 5.

Otros puntos débiles de los multiprocesadores son:

 Difícil determinar un proceso paralelizable. Se trata de un proceso artesanal que se podrá automatizar
sólo si es muy evidente, ya que resulta complejo determinar si una aplicación es o no paralelizable.

 Lentitud en las comunicaciones: Se trata de sistemas con alta latencia, del orden de microsegundo,
cuando tenemos que tener en cuenta que la latencia a memoria local es del orden de pocos
nanosegundos. Si un procesador necesita un dato remoto el rendimiento decrece drásticamente.

Argumentos a favor de los multiprocesadores:

73
Para comprender el impacto de los diferentes factores de rendimiento en un programa paralelo sobre una
arquitectura paralela, es habitual mirarlo desde un punto de vista de procesador individual con todos su
diferentes componentes empleando tiempo de ejecución en el programa, esto es, cuánto tiempo de procesador
emplea en las diferentes actividades para la ejecución de las instrucciones, acceso a los datos en la jerarquía de
la memoria extendida, y coordinar estas actividades con otros procesos. Estos componentes de tiempo pueden
estar relacionados muy directamente con las capacidades de rendimiento del software estudiado en los
apartados anteriores, que nos han ayudado a entender las técnicas de rendimiento software y de hardware. Esta
visión también nos ayuda a comprender cómo es una ejecución paralela.

En la figura (a) se muestra un perfil de un hipotético programa secuencial. En este caso, cerca del 80% del tiempo
de ejecución se emplea en instrucciones de rendimiento, que puede reducirse sólo mediante la mejora ya sea
del algoritmo o del procesador. El otro 20% se emplea bloqueando la memoria del sistema, que puede ser
mejorado mediante la mejora de localidad de datos o el sistema de memoria.

En multiprocesadores, podemos tener una vista similar, en la que no hay tantos factores no ideales. Este punto
de vista es transversal a los modelos de programación: por ejemplo, hay bloqueos de espera para una recepción
completa que es muy similar a que se bloquee a la espera de una lectura remota completa o de que ocurra un
evento de sincronización. Si el mismo programa se paraleliza y se ejecuta en una máquina con cuatro
procesadores, el perfil del tiempo de ejecución para los cuatro procesadores sería el mostrado en la figura (b).
La figura asume un punto de sincronización global al final del programa que hace que los procesos terminen al
mismo tiempo. Nótese que el tiempo de ejecución paralela (55 s) es mayor que un cuarto del tiempo de
ejecución secuencial (100 s); esto es, hemos obtenido una aceleración de sólo 100/55 o 1’8, en lugar de una
aceleración cuádruple que es lo que podríamos esperar. ¿Por qué ocurre esto y qué factores del software o de
programación específicos contribuyen a que se pueda determinar mediante el examen de los componentes de
tiempo de ejecución paralela desde la perspectiva de un procesador individual?

1) Busy-useful. Tiempo que el procesador emplea ejecutando instrucciones que habrían sido ejecutadas
igualmente en el programa secuencial. Asumimos un programa paralelo determinista que deriva
directamente de un algoritmo secuencial, la suma de los tiempos de ocupación útil para todos los
procesadores es el mismo que el tiempo de ocupación útil para una ejecución secuencial.

2) Busy-overhead. Tiempo que el procesador emplea en ejecutar instrucciones que no son necesarias en el
programa secuencial pero sí en el programa paralelo. Esto se corresponde directamente en un trabajo
extra que realiza el programa paralelo.

3) Data-local. Tiempo que el procesador está bloqueado esperando el dato referenciado para ser satisfecho
en la memoria del sistema o para su propio nodo de procesado, esto es, espera para una referencia que
no genera comunicaciones con otros nodos.

4) Data-remote. Tiempo que el procesador está bloqueado esperando por un dato que debe ser
comunicado hacia o desde otros nodos de procesamiento (remotos), ya sea por elementos inherentes o
impropios a la comunicación. Esto representa el coste de la comunicación visto por el procesador.

5) Synchronization: Tiempo empleado en la espera de que otros procesos indiquen la ocurrencia de un


evento que le permita proceder. Esto incluye el desequilibrio de la carga y serialización del programa así
como el tiempo empleado actualmente en la ejecución de operaciones de sincronización y acceso a
variables sincronizadas. Mientras transcurre la espera, el procesador puede repetir el sondeo (polling)
de una variable hasta que el valor de dicha variable cambie, ejecutando de este modo las instrucciones,
o bien puede estar bloqueado, dependiendo de cómo haya sido implementada la sincronización.

Los componentes Synchronization, Busy-overhead, y Data-remote no se encuentran en programas de ejecución


secuencial sobre un sistema uniprocesador y son sobrecargas de cabeceras introducidas para el paralelismo.

74
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

3.2 Taxonomía y modelos de procesamiento paralelo.


Probablemente la clasificación más popular de computadores sea la clasificación o taxonomía de Flynn,
propuesta en los años 1960, aún se utiliza hoy en día. Esta taxonomía de las arquitecturas está basada en la
clasificación atendiendo al flujo de datos e instrucciones en un sistema. Un flujo de instrucciones es el conjunto
de instrucciones secuenciales que son ejecutadas por un único procesador, y un flujo de datos es el flujo
secuencial de datos requeridos por el flujo de instrucciones. Con estas consideraciones, la siguiente figura
muestra esta clasificación de Flynn de los sistemas en cuatro categorías:

Flujos de Datos

Simple Múltiple

Simple SISD SIMD


Flujos de
Instrucciones
Múltiple MISD MIMD

Categorización del hardware, con ejemplos, según en el número de flujos de instrucciones y de datos según la taxonomía de Flynn.

SISD (Single Instruction stream, Single Data stream) Flujo único de instrucciones y Flujo único de datos. Este es
el concepto de arquitectura serie de Von Neumann donde, en cualquier momento, sólo se está ejecutando una
única instrucción. A menudo a los SISD se les conoce como computadores serie escalares. Todas las máquinas
SISD poseen un registro simple que se llama contador de programa (Program Counter, PC) que asegura la
ejecución secuencial (en serie) de las instrucciones del programa. Conforme se van leyendo las instrucciones de
la memoria, el contador de programa se actualiza para que apunte a la siguiente instrucción a procesar en serie.
Prácticamente ningún computador puramente SISD se fabrica hoy en día ya que la mayoría de procesadores
modernos incorporan algún grado de paralelización como es la segmentación de instrucciones o la posibilidad
de lanzar dos instrucciones a un tiempo (superescalares).

MISD (Multiple Instruction stream, Single Data stream) Flujo múltiple de instrucciones y Flujo único de datos.
Esto significa que varias instrucciones actúan sobre el mismo y único flujo de datos. Este tipo de máquinas se
pueden interpretar de dos maneras. Una es considerar la clase de máquinas que requerirían que unidades de
procesamiento diferentes recibieran instrucciones distintas operando sobre los mismos datos. Esta clase de
arquitectura ha sido clasificada por numerosos arquitectos de computadores como impracticable o imposible, y
en estos momentos no existen ejemplos que funcionen siguiendo este modelo. Otra forma de interpretar los
MISD sería como una clase de máquinas donde un mismo flujo de datos fluye a través de numerosas unidades
procesadoras. Arquitecturas altamente segmentadas, como los arrays sistólicos o los procesadores vectoriales,
son clasificadas a menudo bajo este tipo de máquinas. Las arquitecturas segmentadas, realizan el procesamiento
vectorial a través de una serie de etapas, cada una ejecutando una función particular produciendo un resultado
intermedio. La razón por la cual dichas arquitecturas son clasificadas como MISD es que los elementos de un
vector pueden ser considerados como pertenecientes al mismo dato, y todas las etapas segmentadas
representan múltiples instrucciones que son aplicadas sobre ese vector.

SIMD (Single Instruction stream, Multiple Data stream) Flujo de instrucción simple y Flujo de datos múltiple. Esto
significa que una única instrucción es aplicada sobre diferentes datos al mismo tiempo. En las máquinas de este
tipo, las diferentes unidades de procesado son invocadas por una única Unidad de Control. Al igual que las MISD,
las SIMD soportan procesamiento vectorial (matricial) asignando cada elemento del vector a una unidad
funcional diferente para procesamiento concurrente. Por ejemplo, el cálculo del salario para cada trabajador en
una empresa, es repetir la misma operación sencilla para cada trabajador; si se dispone de una arquitectura
SIMD esto se puede calcular en paralelo para cada trabajador. Debido a esta facilidad en la paralelización de
vectores de datos (los trabajadores formarían un vector) se les llama también procesadores matriciales.

75
MIMD (Multiple Instruction stream, Multiple Data stream) Flujo de instrucciones múltiple y Flujo de datos
múltiple. Son máquinas que poseen varias unidades procesadoras en las cuales se pueden realizar múltiples
instrucciones sobre datos diferentes de forma simultánea. Las MIMD son las más complejas, pero son también
las que potencialmente ofrecen una mayor eficiencia en la ejecución concurrente o paralela. Aquí la concurrencia
implica que no sólo hay varios procesadores operando simultáneamente, sino que además hay varios programas
(procesos) ejecutándose también al mismo tiempo, conocido como multiComputador (como es el clúster). Es
posible escribir programas separados que se ejecuten en los diferentes procesadores de un computador MIMD
y conseguir que colaboren para la consecución de un objetivo coordinado más ambicioso. Sin embargo, lo más
habitual es que los programadores escriban un único programa que se ejecute en todos los procesadores del
computador MIMD, introduciendo secuencias condicionales para conseguir que procesadores diferentes
ejecuten secciones de código diferentes. Este estilo de programación se llama Programa Único Datos Múltiples
(Single Program Multiple Data, SPMD), y es el estilo habitualmente utilizado en los computadores MIMD.

Esquemas funcionales de los cuatro tipos de máquinas clasificadas por flujos de instrucciones y de datos:

UC = Unidad de Control. EP = Elemento de Proceso.


UP = Unidad de Procesamiento. FI = Flujo de Instrucciones.
UM = Unidad de Memoria. FD = Flujo de datos.
ML = Memoria Local.

76
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

3.3 Arquitectura MultiProcesadores y MultiComputadores (MIMD).


Surgen del interés por construir un gran computador a base de conectar varios procesadores, para reducir la
relación coste/rendimiento. Lo que da lugar a un amplio abanico de sistemas: tendremos computadores con
varios procesadores acoplados e incluso conjuntos de computadores interconectados entre sí (el clúster es el
caso más extremo). Los puntos clave en el diseño con la red de interconexión y la forma de organizar la jerarquía
de la memoria. La taxonomía de Flynn amplía la clasificación de los procesadores MIMD de la siguiente forma:

Modelo de comunicación
(Disposición lógica – Espacio de direcciones)
Espacio de direcciones Espacio de direcciones
compartido separado / privado
Centralizada
Conexión física UMA - - Poco escalables
en Bus
de la
memoria principal - Distinto software.
Distribuida
(Disposición física) NUMA MPM - Casi igual hardware.
en Red
- Muy escalables.
multiProcesadores multiComputadores
- Comunicación por - Comunicación por paso
variables compartidas. de mensajes.
- Igual software. - Procesos en paralelo.
- Distinto hardware.
- Hilos en paralelo.

Los factores que definen el diseño de un MIMD vienen determinados por el número de procesadores, la conexión
física de la memoria interconectada entre ellos y el modelo de comunicación y compartición de los datos.

Aquellos procesadores MIMD donde el espacio de direcciones es compartido, se dice de multiProcesadores


escalables con memoria compartida; y aquellos donde el espacio de direcciones es separado/privado, de
multiComputadores con paso de mensajes.

Ventajas:
- Tanto los datos, como la sincronización y la coordinación usan variables globales.
- Utiliza un modelo simple de programación.
- Cuenta con un espacio único de memoria.
- Existe una sola copia del sistema operativo (con planificador adecuado).
- Se recurre al uso de hilos de ejecución (threads).
- Los sistemas operativos modernos coordinan la distribución de los recursos.
- Es fácil mover procesos entre procesadores.
- Requieren menos espacio.
- Consumen menos potencia.
- Son más estables.

Inconvenientes:
- Peor rendimiento (performance) de la memoria, que se soluciona con las memorias cachés, pero aparece
el problema de coherencia entre las memorias cachés.
- La red de interconexión (acceso a memoria) es complicada apareciendo modelos dinámicos: bus,
crossbar o multistage, que se saturan rápidamente.
- Soportan pocos procesadores (2-32).
- Resultan ser poco escalables.

77
3.3.1 Arquitectura MultiProcesadores UMA.
Dada la dificultad de reescribir los programas antiguos para que se ejecuten correctamente en un hardware
paralelo, la pregunta natural que cualquiera podría hacerse es qué hacen los diseñadores de computadores para
facilitar esta tarea. Una respuesta a esta pregunta es que, en algunos sistemas, proporcionan un espacio de
direcciones físicas único compartido por todos los procesadores. Así, los programas no tienen que preocuparse
de dónde se van a ejecutar, ya que lo único que necesitan saber es que se van a ejecutar en paralelo. Con este
enfoque, podemos hacer que todas las variables de un programa estén disponibles para todos los procesadores
en cualquier momento. La alternativa es tener espacios de direcciones separados para cada procesador, donde
la compartición debe hacerse en modo explícito. Cuando el espacio de direcciones es común (lo usual en los
chips multinúcleo) el hardware proporciona coherencia de caché para tener una visión coherente de la memoria
compartida.

Se trata de procesadores con su caché propia que se conectan a un bus común para acceder a los recursos
compartidos (memoria, E/S, etc.), por lo que la memoria está físicamente centralizada. Estos sistemas se llaman
multiprocesadores con acceso a memoria uniforme (Uniform Memory Access, UMA) o bien, multiprocesadores
simétricos (Symetric MultiProcessor, SMP).

El tiempo en acceder a la memoria principal es siempre el mismo ya que se encuentra situada en el mismo punto
del sistema, este acceso es el cuello de botella. Cada acceso requiere disponer del bus por lo que se genera
mucho tráfico. Por este motivo los sistemas que emplean la conexión en bus suelen ser poco escalables (de 2 a
32 procesadores). Un mayor número exigiría un elevado ancho de banda en el bus, esto es, el ancho de banda
solicitado por cada procesador crece cada vez más. Sin embargo, este elevado tráfico se reduce gracias a las
memorias cachés que cada procesador posee. Puesto que comparten la memoria principal, pero cada uno posee
su propia memoria caché, es necesario disponer de mecanismos de coherencia de memorias cachés que
mantenga la coherencia de los datos. Para ello se utilizan protocolos de husmeo (snooping) como ISX (Invalid -
Línea inválida, Shared – Línea compartida, eXclusive – Línea exclusiva) similar al de Copy Back. Existe una
comunicación implícita (variables compartidas) en una misma línea replicada en varios procesadores, por lo que
se requiere de hardware adicional para solucionarlo.

Procesador1 Procesador2 Procesadorn

Caché1 Caché2 Cachén

Bus común – Subsistema de interconexión simple

Memoria Principal E/S

Organización clásica de un multiprocesador de memoria compartida.

Como normalmente los procesadores que operan en paralelo comparten datos, es necesario introducir una
coordinación en la operación sobre los datos compartidos; de lo contrario, un procesador podría comenzar a
operar sobre un dato antes de que otro procesador terminase de modificar ese mismo dato. Esta coordinación
se llama sincronización. Cuando la compartición se lleva a cabo en un espacio de direcciones único, es necesario
disponer de un mecanismo separado de sincronización. Una de las alternativas es el uso de bloqueos (lock) para
una variable compartida. En cada instante solo un procesador puede hacerse con el bloqueo, y el resto de los
procesadores interesados en acceder a la variable compartida deben esperar hasta que el primero desbloquee
la variable.
78
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

3.3.2 Arquitectura MultiProcesadores NUMA.


La alternativa a la compartición del espacio de direcciones es que cada procesador disponga de su propio espacio
de direcciones físicas privado.

Estos sistemas se denominan multiprocesadores con acceso a memoria no uniforme (NonUniform Memory
Access, NUMA), en los que, desde un punto de vista lógico, todas las memorias forman un espacio de direcciones
común a todos los procesadores.

La siguiente figura muestra la organización clásica de un multiprocesador con múltiples espacios de direcciones
privados.

Mismo Sistema Operativo

Procesador1 Procesador2 Procesadorn

Caché1 Caché2 Cachén

Memoria1 Memoria2 Memorian

Red de interconexión

Organización clásica de un multiprocesador con múltiples espacios de direcciones privados. Observe que la red de
interconexión no se encuentra entre las cachés y la memoria, sino entre nodos procesador-memoria.

Cada procesador posee una memoria física propia, por lo que la terna procesador-caché-memoria forma un nodo
que se conecta a otros procesadores a través de la red. La memoria está lógicamente distribuida entre los
diversos nodos.

Los direccionamientos a memoria sólo acceden a la red de interconexión si el dato no está en su propia memoria
física, lo que supone aportar menos tráfico por procesador a la misma. Consecuentemente, este tipo de
arquitectura es más escalable admitiendo un mayor número de procesadores.

Además, los accesos a memoria son más rápidos (menor latencia), ya que los direccionamientos a su propia
memoria física no suponen acceder a la red de interconexión. Sin embargo, en términos generales, el tiempo de
acceso a memoria es variable (no uniforme), pues dependerá de la memoria a la que deba dirigirse.

Como era de esperar, es más difícil programar un multiprocesador NUMA que un multiprocesador UMA, y como
el código es compatible, puede aprovecharse. Pero los sistemas NUMA escalan mejor a tamaños (número de
procesadores) más grandes y tienen latencias de acceso a memoria menores que el acceso a la memoria cercana
al procesador.

La coherencia de cachés es más difícil de mantener en hardware, por lo que se utilizan directorios. Obliga a estar
atentos con las falsas comparticiones (false sharing).

Hace 10 años eran sistemas poco habituales, se utilizaban como supercomputadores para la investigación. Sin
embargo, hoy en día contamos con Intel Core con QuickPath Interconnect y AMD Opteron con HyperTransport.

79
3.3.3 Arquitectura MultiComputadores MPM.

La alternativa a la compartición del espacio de direcciones es que cada procesador disponga de su propio espacio
de direcciones físicas privado. Las comunicaciones se tienen que hacer mediante un explícito paso de mensajes
(Message Passing Machines, MPM) que tradicionalmente es el nombre que recibe este tipo de computadores.
Dado que se dispone de rutinas para enviar y recibir mensajes, la coordinación se realiza con el paso de mensajes,
ya que un procesador sabe cuándo se ha enviado un mensaje y el procesador que lo recibe sabe cuándo le llega
un mensaje. Si el procesador que envía un mensaje necesita confirmación de que el mensaje ha llegado a su
destinatario, el procesador que lo recibe debe enviar un mensaje de confirmación al remitente.

Sistema Operativo A Sistema Operativo B Sistema Operativo C

Procesador1 Procesador2 Procesadorn

Caché1 Memoria1 Caché2 Memoria2 Cachén Memorian

Bus1 Bus2 Busn

Red de interconexión – Subsistema de interconexión complejo

Algunas aplicaciones concurrentes se ejecutan bien en un hardware paralelo, independientemente de si es de


memoria compartida o de paso de mensajes. En particular, el paralelismo a nivel de tareas y aplicaciones con
pocas comunicaciones, como por ejemplo la búsqueda en la web, servicios de correo electrónico y servidores de
ficheros, no necesitan de un direccionamiento compartido para ejecutarse eficientemente.

Ha habido varios intentos de construir computadores de altas prestaciones basados en redes de paso de
mensajes de altas prestaciones, que ofrecían comunicaciones con mejores prestaciones que los clústeres con
redes de área local. El problema fue que eran mucho más caros. Dado este elevado coste, pocas aplicaciones
podrían justificar la necesidad de unas comunicaciones con mejores prestaciones. De este modo, los clústeres se
convirtieron en el ejemplo de computador paralelo de paso de mensajes más extendido en la actualidad. Los
clústeres son generalmente un conjunto de computadores convencionales conectados a través de sus puertos
de E/S utilizando conmutadores de red y cables estándar. Cada uno ejecuta una copia diferente del sistema
operativo. Los servicios de internet actuales confían en clústeres de servidores y conmutadores convencionales.

Una de las desventajas de los clústeres ha sido que el coste de administrar un clúster con n computadores es
prácticamente el mismo que el de administrar n computadores independientes, mientras que el coste de
administrar un multiprocesador de memoria compartida con n procesadores es aproximadamente el mismo que
el de administrar un único computador.

Esta debilidad es una de las razones de la popularidad de las máquinas virtuales, que facilitan la administración
de los clústeres. Por ejemplo, las máquinas virtuales hacen imposible parar y comenzar programas
atómicamente, lo que simplifica las actualizaciones de software. Las máquinas virtuales pueden incluso trasladar
en un clúster un programa de un computador a otro sin detener el programa en caso de un fallo del hardware.

80
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Otra desventaja de los clústeres es que los procesadores están normalmente conectados a través de los puertos
de E/S de cada computador, mientras que los núcleos de un multiprocesador están conectados a través de los
puertos de conexión con memoria, que tienen mayor ancho de banda y menor latencia, que permiten por tanto,
una comunicación con prestaciones mucho más elevadas.

Una debilidad final es el sobrecoste de la división de la memoria: un clúster con n máquinas tiene n memorias
independientes y n copias del sistema operativo, mientras que un multiprocesador de memoria compartida
permite que un único programa utilice casi toda la memoria del computador y sólo necesita una copia del sistema
operativo.

Uno de los puntos débiles del clúster, es tener memorias separadas para la memoria principal de usuario, que se
convierte en una ventaja para la disponibilidad del sistema. Como un clúster es un conjunto de computadores
independientes conectados a través de una red de área local, es mucho más fácil remplazar una máquina sin
apagar el sistema. Fundamentalmente, las direcciones compartidas implican que es difícil aislar un procesador y
reemplazarlo sin que el sistema operativo tenga que hacer un esfuerzo heroico. Por el contrario, como el
software del clúster es una capa que se ejecuta sobre el sistema operativo local de cada computador, es mucho
más sencillo desconectar y reemplazar la máquina estropeada.

Puesto que los clústeres se construyen con computadores completos y redes escalables independientes, este
aislamiento facilita además la expansión del sistema sin necesidad de cerrar la aplicación que se está ejecutando
en la capa más elevada del clúster.

Los computadores que realizan la comunicación con paso de mensajes en lugar de memoria son mucho más
fáciles de diseñar. La ventaja, desde el punto de vista del programador, es la comunicación explícita, lo que
significa que hay menos sorpresas en las prestaciones que con los computadores con memoria compartida. El
inconveniente es la portabilidad, es más difícil reescribir un programa secuencial para un sistema con paso de
mensajes porque cada comunicación debe identificarse con anterioridad o el programa no funcionará
correctamente. Los computadores con memoria compartida permiten que el hardware averigüe qué dato debe
compartirse, lo que facilita la portabilidad.

A la vista de los pros y contras de la comunicación implícita, hay diferentes opiniones sobre cuál es el camino
más corto hacia las altas prestaciones Otros aspectos, como menor coste, alta disponibilidad, eficiencia
energética mejorada y posibilidad de expansión rápida e incremental, hacen que los clústeres sean muy
atractivos para los proveedores de servicio en la web. Los motores de búsqueda, que millones de nosotros
utilizamos cada día, dependen de esta tecnología; Google, eBay, Microsoft, Yahoo! y otros, tienen varios centros
de datos con clústeres de decenas de miles de procesadores. Claramente, el uso de múltiples procesadores en
las compañías de servicios en internet ha sido altamente exitoso.

81
Podríamos decir que los clústeres están compuestos por una serie de computadores independientes, con su
propia memoria (arquitectura NUMA), conectados a través de algún sistema de entrada/salida (Ej. Ethernet).
Donde a cada uno de dichos computadores se denomina nodo, que ejecuta su propio sistema operativo, aunque
en conjunto, se muestran como un sólo sistema.

A la interfaz entre el sistema operativo y los procesos se le llama Middleware, encargado del lanzamiento de
tareas, de la migración de procesos, del balanceo de carga, detección de fallos y del descubrimiento de nodos.

Los clústeres se pueden clasificar acorde con su función:

 HPC: Clústeres de altas prestaciones.


- Ejecutan procesos que requieren de una gran cantidad de cálculos y uso de memoria.
- Se busca maximizar el número de operaciones por segundos (TeraFlops).
- Uso para el cómputo científico (Simulaciones en ingeniería…).
- Pretenden sustituir a los grandes y costosos supercomputadores.

 HAC: Clústeres de alta disponibilidad.


- Proveen servicios de forma ininterrumpida.
- Implementan sistemas de detección y recuperación de fallos.
- Se usan nodos de reserva y, en caso de fallo, se migran los nodos defectuosos.
- Servicios usuales: Bases de datos, sistemas de ficheros, servidores de correo y servidores Web.
- Intenta mantener en todo momento la prestación del servicio.
- Fiabilidad: (MTTF, Mean Time To Failure)
- Disponibilidad
- Facilidad de Mantenimiento

 HTC: Clústeres de alta eficiencia.


- Buscan maximizar el número transacciones a ejecutar simultáneamente con una latencia razonable.
- Sistemas de balanceo de carga: gestión de la carga de trabajo de los nodos (en ocasiones se dedican
nodos a sólo esta tarea).
- Servicios usuales: Servidores DNS, búsqueda web, pago electrónico, market…

 LBC: Clústeres de alta eficiencia o balanceo de carga.


- Servidores DNS, buscadores Web, etc.

82
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

3.4 Programación de la memoria compartida.

3.4.1 API: OpenMP

En sistemas con memoria compartida (UMA y NUMA), se hace uso del API OpenMP para la programación de
multiprocesadores con memoria compartida y multihilos.

OpenMP se nos presenta como un modelo de programación paralela, a modo de una pequeña API. Resuelve el
paralelismo de memoria compartida, lo que resulta fácil para los bucles (paralelizables directa o indirectamente).
Es en sí mismo un conjunto de extensiones a lenguajes de programación existentes (Fortran, C, C++).

OpenMP proporciona una serie de directivas de compilación con las que se indican si un determinado bloque de
código ha de ejecutarse de forma secuencial o de forma paralela. Además, nos ofrece una colección de funciones
para la identificación y la sincronización de procesos.

Sintaxis de OpenMP:

- Pragma en C y C++:
#pragma omp construct [clause [clause]…]

- En Fortran, directivas :
C$OMP construct [clause [clause]…]
!$OMP construct [clause [clause]…]
*$OMP construct [clause [clause]…]

No obstante, un programa puede ser compilado por compiladores que no soportan OpenMP. En caso de hacerlo
no olvidar verificar “Compatiblidad OpenMP” en el compilador.

Ejemplo:

int main(int argc, char *argv[])


{
const int N = 100000;
int i, a[N], b[N], c[N];
#pragma omp parallel for
for (i = 0; i < N; i++)
c[i] = a[i] + b[i];
}

Cuando se paraleliza un bucle for, automáticamente se distribuyen las iteraciones del bucle a cada uno de los
hilos. Al estar los datos en la memoria principal compartida, no hace transferirlos explícitamente a cada
procesador. Cuando comienzan los accesos, los datos acaban en la memoria caché.

83
Ejemplo de cómo paralelizar un bucle sencillo:

Programa secuencial:
void main() {
double a[1000], b[1000], c[1000];
for (int i = 0; i<1000; i++) {
a[i] = b[i] + c[i];
}
}

Programa paralelo:
void main() {
double a[1000], b[1000], c[1000];
#pragma omp parallel for
for (int i = 0; i<1000; i++) {
a[i] = b[i] + c[i];
}
}

Regiones paralelas y críticas:

OpenMP no sólo permite paralelizar bucles, sino también bloques completos de instrucciones.

#pragma omp parallel


{
/* Región en ejecución paralela
por cada thread */
}

Control de tareas en
f(thread_id)

Se basa en el uso de un hilo maestro que ejecuta el código secuencial y cuando se encuentra una sección paralela,
la ejecución del código se distribuye entre todos los procesadores disponibles.

Los hilos (threads) se comunican utilizando variables compartidas. De esta forma el uso inadecuado de variables
compartidas origina carreras, por lo que se debe usar sincronización o exclusión para evitarlas. Hay que tener en
cuenta que la sincronización es muy costosa en tiempo, por los que se usará lo menos posible.

OpenMP proporciona funciones para conocer el identificador de cada hilo, así como el número total de hilos que
están ejecutándose en una región paralela.

84
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Declarando el ámbito de datos.

 shared: Variable es compartida por todos los procesos. Ej: vectores del proceso.
 private: Cada proceso tiene una copia.

#pragma omp parallel for shared(a, b, c, n) private(i, temp)


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

Hay reglas para decidir por defecto el ámbito, pero es mejor no arriesgarse.

Cuidado con el false sharing: ¿Qué pasa si n=2 y tenemos definido float a[2], b[2], c[2]; ?

Ejemplo:

Conocidas las siguientes directivas:

omp_get_num_threads() Devuelve el número actual de threads


omp_get_thread_num() Devuelve el identificador de ese thread
omp_set_num_threads(n) Activa un número de threads

#include <omp.h>
main() {
int nth, tid;

//Declara una región paralela, la variable tid es privada


#pragma omp parallel private(tid)
{
/* Obtiene el identificador del hilo*/
tid = omp_get_thread_num();
nth = omp_get_num_threads();
printf("Hola mundo, hilo = %d de %d\n", tid, nth);
}

//Volvemos a una sección secuencial y los hilos se unen.


}

Secciones (una por hilo).


#pragma omp parallel sections
{
#pragma omp section
{
printf("id s1=%d,\n", omp_get_thread_num());
for (i = 0; i<tam / 2; i++)
y[i] = a * x[i] + y[i];
}
#pragma omp section
{
printf("id s2=%d,\n", omp_get_thread_num());
for (i = tam / 2; i<tam; i++)
y[i] = a * x[i] + y[i];
}
}
85
OpenMP ofrece diversos mecanismos de sincronización:

o Barreras: Una vez que se detecta la barrera la ejecución de los hilos se para hasta que todos los hilos
lleguen a la barrera.
#pragma omp barrier

Los threads se detienen hasta que alcancen la barrera.


nt = omp_get_num_threads();
#pragma omp parallel private (i, id)
{
id = omp_get_thread_num();
for (i = id*tam / nt; i<(id + 1)*tam / nt; i++) {
y[i] = a*x[i] + y[i];
}
#pragma omp barrier
// Aquí seguro que todo y[] ya está actualizado
for (i = id*tam / nt; i<(id + 1)*tam / nt; i++) {
z[i] = b + y[tam - i - 1];
}
}
Caso de estudio: ¿qué pasaría si se fusionan los bucles?

o Exclusión de las secciones críticas: Sólo acede un hilo al recurso al mismo tiempo, obligando a la
ejecución secuencial de todos los hilos
#pragma omp critical
{
//acceso a un recurso compartido. Por ejemplo, la consola.
}

Exclusión Mutua:
#pragma omp parallel shared(x, y)
{ …
#pragma omp critical (section1)
actualiza(x); //sólo un thread

usa(x);

#pragma omp critical(section2)
actualiza(y); // sólo un thread

usa(y);
}

86
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

OpenMP permite la planificación de tareas (SCHEDULE):

Diferentes formas de asignar iteraciones a threads:

schedule (static [,chunk]) “chunk” iteraciones se asignan de manera estática a los threads en round-robin.
schedule (dynamic [,chunk]) Cada thread toma “chunk” iteraciones cada vez que está sin trabajo.
schedule (guided [,chunk]) Cada thread toma progresivamente menos iteraciones (dinámicamente).

Existe un igual número de iteraciones para static como para dynamic, y resulta exponencialmente decreciente
para guided. Teniendo en cuenta que static es para bucles simples y que dynamic es si la carga varía.

87
Variables reduction: Operaciones colectivas sobre elementos de un array:
asum = 0.0;
aprod = 1.0;
#pragma omp parallel for reduction(+:asum)
reduction(*:aprod)
for (i=0; i<n; i++ ) {
asum = asum + a[i];
aprod = aprod * a[i];
}

Ejemplo: Cálculo de PI

[ secuencial ] 1
𝐴𝑟𝑐𝑇𝑔′ (𝑥) =
1 + 𝑥2 1
1 1 𝜋
static long num_steps = 100000; 𝜋 ⇒∫ = 𝐴𝑟𝑐𝑇𝑔(𝑥)| = −0
double step; 𝐴𝑟𝑐𝑇𝑔(1) = 1 + 𝑥 2 0 4
void main () { 4 0

int i; 𝐴𝑟𝑐𝑇𝑔(0) =0 }
double x, pi, sum = 0.0;
step = 1.0/(double) num_steps;
for (i=1; i<= num_steps; i++) {
x = (i - 0.5) * step;
sum = sum + 4.0 / (1.0 + x * x);
}
pi = step * sum;
}

[ reduction ]
#include <omp.h>
static long num_steps = 100000;
double step;
void main() {
int i;
double x, pi, sum = 0.0;
step = 1.0 / (double) num_steps;
omp_set_num_threads(2);
#pragma omp parallel for reduction(+:sum) private(x)
for (i=1; i<=num_steps; i++) {
x = (i - 0.5) * step;
sum = sum + 4.0 / (1.0 + x * x);
}
pi = step * sum;
}

[ reducción artesanal ]
#include <omp.h>
static long num_steps = 100000;
double step;
void main() {
int i;
double x, pi, sum[2];
step = 1.0 / (double) num_steps;
omp_set_num_threads(2);
#pragma omp parallel
{
double x;
int id;
id = omp_get_thread_num();
for (i = id * num_steps / 2, sum[id] = 0.0; i <= (id + 1) * num_steps / 2; i++) {
x = (i + 0.5) * step;
sum[id] += 4.0 / (1.0 + x * x);
}
}
for (i = 0, pi = 0.0; i < 2; i++)
pi += sum[i] * step;
}

88
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplos con OpenMP.

Dado los siguientes códigos de programas en C, paralelízelos utilizando OpenMP:

Ejemplo 1.

Enunciado:
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[])


{
int nthreads, tid;
tid = omp_get_thread_num();
printf("Hola desde el hilo = %d\n", tid);
/* Solo el nodo maestro hace lo siguiente: */
if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Numero de hilos = %d\n", nthreads);
}
}

Solución:
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[])


{
int nthreads, tid;
#pragma omp parallel private(tid)
{
tid = omp_get_thread_num();
printf("Hola desde el hilo = %d\n", tid);
/* Solo el nodo maestro hace lo siguiente: */
if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Numero de hilos = %d\n", nthreads);
}
}
}

89
Ejemplo 2.

Enunciado:
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[])


{
int i, n;
float a[100], b[100], sum;

/* Inicialización de la variable 'sum' y de los vectores 'a' y 'b' */


n = 100;
for (i=0; i < n; i++)
a[i] = b[i] = i * 1.0;
sum = 0.0;
for (i=0; i < n; i++)
sum = sum + (a[i] * b[i]);

printf(" Sum = %f\n",sum);


}

Solución:
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[])


{
int i, n;
float a[100], b[100], sum;

/* Inicialización de la variable 'sum' y de los vectores 'a' y 'b' */


n = 100;
#pragma omp parallel for
for (i=0; i < n; i++)
a[i] = b[i] = i * 1.0;

sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (i=0; i < n; i++)
sum = sum + (a[i] * b[i]);

printf(" Sum = %f\n",sum);


}

90
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo 3.

Enunciado:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

#define CHUNKSIZE 10
#define N 100

int main (int argc, char *argv[])


{
int nthreads, tid, i, chunk;
float a[N], b[N], c[N];
/* Inicializaciones */
for (i=0; i < N; i++)
a[i] = b[i] = i * 1.0;
/* chunk (trozo) sirve para indicar el tamaño de datos a tratar
por cada thread en la planificación que se decida:
'static', 'dynamic' o 'guided'
*/
chunk = CHUNKSIZE;
tid = omp_get_thread_num();
if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Numero de hilos = %d\n", nthreads);
}
printf("Hilo %d comenzando...\n",tid);
for (i=0; i<N; i++)
{
c[i] = a[i] + b[i];
printf("Hilo %d: c[%d]= %f\n",tid,i,c[i]);
}
}

91
Solución:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

#define CHUNKSIZE 10
#define N 100

int main (int argc, char *argv[])


{
int nthreads, tid, i, chunk;
float a[N], b[N], c[N];
/* Inicializaciones */
#pragma omp parallel for
for (i=0; i < N; i++)
a[i] = b[i] = i * 1.0;
/* chunk (trozo) sirve para indicar el tamaño de datos a tratar
por cada thread en la planificación que se decida:
'static', 'dynamic' o 'guided'
*/
chunk = CHUNKSIZE;
#pragma omp parallel shared(a, b, c, nthreads, chunk) private(i, tid)
{
tid = omp_get_thread_num();
if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Numero de hilos = %d\n", nthreads);
}
printf("Hilo %d comenzando...\n",tid);

#pragma omp for schedule(dynamic, chunk)


for (i=0; i<N; i++)
{
c[i] = a[i] + b[i];
printf("Hilo %d: c[%d]= %f\n",tid,i,c[i]);
}
}
}

92
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo 4.

Enunciado:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

#define N 50

int main (int argc, char *argv[])


{
int i, nthreads, tid;
float a[N], b[N], c[N], d[N];

/* Inicializaciones de los vectores: */


for (i=0; i<N; i++) {
a[i] = i * 1.5;
b[i] = i + 22.35;
c[i] = d[i] = 0.0;
}
tid = omp_get_thread_num();
if (tid == 0) {
nthreads = omp_get_num_threads();
printf("Numero de hilos = %d\n", nthreads);
}
printf("Hilo %d empezando...\n",tid);
printf("Hilo %d procesando seccion 1\n",tid);
for (i=0; i<N; i++) {
c[i] = a[i] + b[i];
printf("Hilo %d: c[%d]= %f\n",tid,i,c[i]);
}
printf("Hilo %d procesando seccion 2\n",tid);
for (i=0; i<N; i++) {
d[i] = a[i] * b[i];
printf("Hilo %d: d[%d]= %f\n",tid,i,d[i]);
}
printf("Hilo %d terminado.\n",tid);
}

93
Solución:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

#define N 50

int main (int argc, char *argv[])


{
int i, nthreads, tid;
float a[N], b[N], c[N], d[N];

/* Inicializaciones de los vectores: */


#pragma omp parallel for
for (i=0; i<N; i++) {
a[i] = i * 1.5;
b[i] = i + 22.35;
c[i] = d[i] = 0.0;
}
#pragma omp parallel shared(a, b, c, d) private(i, tid)
{
tid = omp_get_thread_num();
#pragma omp sections nowait
{
#pragma omp section
{
if (tid == 0) {
nthreads = omp_get_num_threads();
printf("Numero de hilos = %d\n", nthreads);
}
}
#pragma omp section
{
printf("Hilo %d empezando...\n",tid);
printf("Hilo %d procesando seccion 1\n",tid);
for (i=0; i<N; i++) {
c[i] = a[i] + b[i];
printf("Hilo %d: c[%d]= %f\n",tid,i,c[i]);
}
}
#pragma omp section
{
printf("Hilo %d procesando seccion 2\n",tid);
for (i=0; i<N; i++) {
d[i] = a[i] * b[i];
printf("Hilo %d: d[%d]= %f\n",tid,i,d[i]);
}
printf("Hilo %d terminado.\n",tid);
}
}
}
}

94
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo 5.

Enunciado:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

#define NRA 62 /* número de filas de la matriz A */


#define NCA 15 /* número de columnas en la matriz A */
#define NCB 7 /* número de columnas en la matriz B */

int main (int argc, char *argv[])


{
int tid, nthreads, i, j, k, chunk;
double a[NRA][NCA], /* matriz A a multiplicar */
b[NCA][NCB], /* matriz B a multiplicar */
c[NRA][NCB]; /* matriz C resultado */
chunk = 10; /* Establecer el tamaño de los trozos para cada hilo en cada iteración */
tid = omp_get_thread_num();
if (tid == 0) {
nthreads = omp_get_num_threads();
printf("Comenzando ejemplo multiplicación de matrices con %d hilos\n",nthreads);
printf("Inicializando matrices...\n");
}
/*** Inicialización de las matrices: ***/
for (i=0; i<NRA; i++)
for (j=0; j<NCA; j++)
a[i][j]= i+j;
for (i=0; i<NCA; i++)
for (j=0; j<NCB; j++)
b[i][j]= i*j;
for (i=0; i<NRA; i++)
for (j=0; j<NCB; j++)
c[i][j]= 0;
/*** Do matrix multiply sharing iterations on outer loop ***/
/*** Visualizar quién hace qué iteración ***/
printf("Hilo %d comenzando multiplicación de matriz ...\n",tid);
for (i=0; i<NRA; i++) {
printf("Hilo=%d hizo fila=%d\n",tid,i);
for(j=0; j<NCB; j++)
for (k=0; k<NCA; k++)
c[i][j] += a[i][k] * b[k][j];
}
/*** Imprimir resultados ***/
printf("******************************************************\n");
printf("Matriz resultado: \n");
for (i=0; i<NRA; i++) {
for (j=0; j<NCB; j++)
printf("%6.2f ", c[i][j]);
printf("\n");
}
printf("******************************************************\n");
printf ("Fin.\n");
}

95
Solución:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

#define NRA 62 /* número de filas de la matriz A */


#define NCA 15 /* número de columnas en la matriz A */
#define NCB 7 /* número de columnas en la matriz B */

int main (int argc, char *argv[])


{
int tid, nthreads, i, j, k, chunk;
double a[NRA][NCA], /* matriz A a multiplicar */
b[NCA][NCB], /* matriz B a multiplicar */
c[NRA][NCB]; /* matriz C resultado */
chunk = 10; /* Establecer el tamaño de los trozos para cada hilo en cada iteración */

/* Generar una región paralela alcance explícitamente todas las variables */


#pragma omp parallel shared(a, b, c, nthreads, chunk) private(tid, i, j, k)
{
tid = omp_get_thread_num();
if (tid == 0) {
nthreads = omp_get_num_threads();
printf("Comenzando ejemplo multiplicación de matrices con %d hilos\n",nthreads);
printf("Inicializando matrices...\n");
}
/*** Inicialización de las matrices: ***/
#pragma omp for schedule (static, chunk)
for (i=0; i<NRA; i++)
for (j=0; j<NCA; j++)
a[i][j]= i+j;
#pragma omp for schedule (static, chunk)
for (i=0; i<NCA; i++)
for (j=0; j<NCB; j++)
b[i][j]= i*j;
#pragma omp for schedule (static, chunk)
for (i=0; i<NRA; i++)
for (j=0; j<NCB; j++)
c[i][j]= 0;
/*** Do matrix multiply sharing iterations on outer loop ***/
/*** Visualizar quién hace qué iteración ***/
printf("Hilo %d comenzando multiplicación de matriz ...\n",tid);
#pragma omp for schedule (static, chunk)
for (i=0; i<NRA; i++) {
printf("Hilo=%d hizo fila=%d\n",tid,i);
for(j=0; j<NCB; j++)
for (k=0; k<NCA; k++)
c[i][j] += a[i][k] * b[k][j];
}
/*** Imprimir resultados ***/
printf("******************************************************\n");
printf("Matriz resultado: \n");
#pragma omp for schedule (static, chunk)
for (i=0; i<NRA; i++) {
for (j=0; j<NCB; j++)
printf("%6.2f ", c[i][j]);
printf("\n");
}
printf("******************************************************\n");
printf ("Fin.\n");
}
}

96
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo 6.

Enunciado:
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#define VECLEN 100
float a[VECLEN], b[VECLEN], sum;

float dotprod () {
int i,tid;
tid = omp_get_thread_num();
for (i=0; i < VECLEN; i++) {
sum = sum + (a[i]*b[i]);
printf(" tid= %d i=%d\n",tid,i);
}
return(sum);
}

int main (int argc, char *argv[]) {


int i;
/* Inicialización de los vectores 'a' y 'b' */
for (i=0; i < VECLEN; i++)
a[i] = b[i] = 1.0 * i;
sum = 0.0;
dotprod();
printf("Suma = %f\n",sum);
return 0;
}

Solución:
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#define VECLEN 100
float a[VECLEN], b[VECLEN], sum;

float dotprod () {
int i,tid;
tid = omp_get_thread_num();
#pragma omp for reduction(+:sum)
for (i=0; i < VECLEN; i++) {
sum = sum + (a[i]*b[i]);
printf(" tid= %d i=%d\n",tid,i);
}
return(sum);
}

int main (int argc, char *argv[]) {


int i;
/* Inicialización de los vectores 'a' y 'b' */
#pragma omp parallel for
for (i=0; i < VECLEN; i++)
a[i] = b[i] = 1.0 * i;
#pragma omp parallel shared(sum)
{
sum = 0.0;
dotprod();
printf("Suma = %f\n",sum);
}
return 0;
}
97
3.4.2 API: MPI

Las plataformas de destino donde usar MPI (Message Passing Interface) son los sistemas con memoria
distribuida son, incluyendo MPM, los clústeres SMP, clústers de estaciones de trabajo y redes heterogéneas. Para
realizar comunicaciones de cierta información en este tipo de sistemas, se hace uso de la librería MPI, que
permite mandar y recibir información entre diferentes espacios de memoria. Esta librería implementa una serie
de protocolos de comunicación para transmitir información entre los nodos del clúster.

A diferencia del API OpenMP del Api MPI, se ejecuta sobre un modelo de memoria distribuida y no uniforme
(NUMA), lo que obliga a hacer un uso extenso de transferencia de datos entre los nodos, lo que conlleva latencia,
overhead, así como de disponer de mecanismos de sincronización entre dichos nodos.

Se trata de una especificación para librerías de paso de mensajes, diseñada para ser un estándar para
computación paralela en sistemas con memoria distribuida y que use para comunicarse paso de mensajes. Las
comunicaciones entre los nodos pueden ser punto a punto o bien multi-punto. Este estándar pretende ser
práctico, eficiente y flexible.

Sabemos entonces que MPI, es una especificación para librerías de paso de mensajes, un estándar mantenido
por el MPI-Forum y muchas otras organizaciones; una biblioteca de funciones que implementan la ejecución
distribuida de un programa mediante paso de mensajes.

Por tanto, las razones para usar MPI son principalmente las siguientes:

 Estandarización: MPI es la única librería para paso de mensajes que se considera un estándar. Es
soportado por prácticamente todas las plataformas HPC.

 Portabilidad: No hay necesidad de modificar el código fuente cuando portamos nuestra aplicación a una
plataforma distinta que soporte MPI, aunque sí requiere una nueva recompilación.

 Prestaciones. Ciertas implementaciones propietarias podrían explotar las características del hardware
para optimizar las prestaciones.

 Funcionalidad: Cuenta con alrededor de 115 rutinas.

 Disponibilidad: Están disponibles una gran variedad de implementaciones, tanto públicas como privadas.
La versión MPI-2 está disponible para los lenguajes C++ y Fortran 90.

Existen múltiples implementaciones reales del estándar MPI como: MPICH, LAM/MPI, OpenMPI, MPI-CH, …

Se puede desarrollar un código híbrido entre OpenMP y MPI, donde cada proceso MPI podrá estar en varios hilos
(threads), esto es, podrá tener un número determinado de hilos asociados. Se crea por tanto un Simple Program
Multiple Data, que no hay que confundirlo con Núcleo Vectorial SIMD.

Contamos con los siguientes modelos de programación según sea su memoria:

- Con memoria compartida: [OpenMP] Distintas CPU/núcleos pueden acceder directamente a las
variables del programa en memoria.

- Con memoria distribuida: [MPI] Asume que cada CPU sólo tiene acceso a su propio espacio de memoria.

Especificar este paralelismo explícito es responsabilidad del programador, de la correcta identificación del
paralelismo, así como de la implementación del algoritmo resultante usando funciones MPI; mientras que en un
programa paralelo, el número de procesos dedicados a correr es estático, no se pueden añadir nuevas tareas
dinámicamente durante el tiempo de ejecución. Lo que MPI-2 viene a solucionar.

98
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

A diferencia de OpenMP, que sólo puede trabajar en un sistema de memoria compartida (p. ej. un procesador
multinúcleo), con MPI se trata de un único espacio compartido de memoria, lo que permite:

 Ejecutar un programa paralelo en el que múltiples procesos cooperan entre sí.


 Cada proceso (residiendo en una CPU/núcleo) sólo tiene acceso a su propio espacio de memoria.
 Para acceder/alterar memoria, variables o información de otro proceso necesita intercambiar
mensajes con él.

Una ventaja de un modelo de programación distribuida (como MPI) es que le permite trabajar en cualquier tipo
de entorno paralelo:

- Procesador multinúcleo.
- Colección de servidores distribuidos (clúster).
- Entorno grid o cloud.

Conceptos Básicos de MPI.

 Hay que indicar al compilador que enlace el código objeto del programa C con la biblioteca de funciones
de MPI: #include <mpi.h>

 MPI opera a través de llamadas a funciones.

- Nomenclatura:
MPI_Nombre-de-la-función

- Todas las funciones devuelven un código de error que indica si se han ejecutado con éxito:

if (err == MPI_SUCCESS)
{
... /* la función se ejecutó correctamente */
}
else {
... /* la función ocasionó un error */
}

 Comunicadores:

- Un comunicador define un grupo de procesos a los que se aplicará un comando en particular.

- Todas las funciones de comunicación de MPI tienen como uno de sus parámetros un comunicador,
que define el contexto en el que la comunicación tendrá lugar.

- El comunicador predefinido MPI_COMM_WORLD consta de todos los procesos ejecutándose cuando


comienza la ejecución del programa; es decir, Incluye todos los procesos del programa. Es el
comunicador utilizado al programar en MPI a nivel básico.
99
Modelo de programación.

Se crea un único fichero de código y cada nodo tiene su propia copia del ejecutable, que incluye bifurcaciones
según el identificador (rango) del propio proceso, así, cada proceso ejecuta algo diferente.

if (id_proceso == 0) {
// Proceso maestro
}
else {
// Proceso esclavo
}

También puede existir un mpd daemon, que se ejecuta en cada nodo del clúster en espera de envío de procesos
para levantarlos.

Con la orden, desde consola, mpirun –np 4 nombre_archivo indicamos al sistema operativo que ejecute el
programa nombre_archivo utilizando, en este caso, 4 procesadores.

Tipos de datos básicos de MPI.

Por razones de portabilidad, MPI tiene una serie de tipos de datos predefinidos. Los programadores pueden
también crear sus propios tipos de datos. Notar que los tipos MPI_BYTE and MPI_PACKED no corresponden a
tipos estándar de C.

Así pues, MPI tiene constantes que definen los tipos de datos básicos. Cada tipo de dato básico de C tiene su
equivalente MPI, de la forma MPI_tipo, que debe ser usado en las llamadas de las funciones en C.

Aunque los programadores pueden crear sus propios tipos, los más más utilizados, con su correspondencia a los
tipos de datos en C, son los siguientes:

TIPO DE DATO MPI TIPO DE DATO C


MPI_CHAR signed char
MPI_SHORT signed short int
MPI_INT signed int (int)
MPI_LONG signed long int
MPI_UNSIGNED_CHAR unsigned char
MPI_UNSIGNED_SHORT unsigned short int
MPI_UNSIGNED unsigned int
MPI_UNSIGNED_LONG unsigned long int
MPI_FLOAT float
MPI_DOUBLE double
MPI_LONG_DOUBLE long double
MPI_BYTE 8 dígitos binarios
MPI_PACKED Datos empaquetados
o desempaquetados con
MPI_Pack / MPI_Unpack

100
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Definiciones de funciones MPI:

int MPI_Init(int *argc, char *argv)

- Debe ser la primera función llamada y sólo se indica una vez, ya que inicializa el entorno MPI.
- Lanza los procesos MPI en cada nodo.
- Sus parámetros son punteros a los parámetros de la función main del programa: argc y argv. Es decir,
toma como parámetros la dirección de argc y la de argv.
- Pasa los argumentos recibidos en la línea de comando al proceso lanzador de MPI.

int MPI_Finalize(void)

- Función que debe ser llamada al finalizar el programa, cierra adecuadamente el entorno MPI.
- El usuario debe asegurarse que todas las comunicaciones han sido completadas antes de llamar a la
función MPI_Finalize.
- Después de ella, ninguna otra función MPI puede ser llamada (incluyendo MPI_Init).

int MPI_Comm_size(MPI_Comm comm, int *totTasks)

- Devuelve el número de procesos en el grupo del comunicador MPI en totTasks, suele ser NCPUs - 1;
esto es, el número de procesos que participan en un comunicador. Si éste es el comunicador global
predefinido MPI_COMM_WORLD, indica el número total de procesos involucrados en el programa.
- [ IN comm ] : Comunicador.
- [ OUT size ] : Número de procesos en el grupo del comunicador comm.

int MPI_Comm_rank(MPI_Comm comm, int *task_id)

- Devuelve el rango (el identificador) del proceso actual (de 0 a totTasks-1) en un comunicador
(*task_id); esto es, un número de proceso (“rank”) del proceso maestro MPI, que tiene rank = 0.
- Cada comunicador contiene un grupo de procesos, cada uno de los cuales tiene un identificador
único, un rango (task_id), que es un número entero que comienza en 0: (0, 1, 2, 3, ...).
- [ IN comm ] : Comunicador.
- [ OUT rank ] : Rango del proceso que llama en el grupo del comunicador comm.

int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)

- Envío básico bloqueante. La función retorna cuando el buffer de la aplicación está libre. A saber que
existe una versión no bloqueante: Isend.

int MPI_Recv(void *rbuf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm,
MPI_Status *status)

- Recibe un mensaje (bloqueante). A saber que existe una versión no bloqueante: Irecv.

MPI_Get_processor_name(char *name, int *resultlen)

- Devuelve el nombre del procesador en que la función fue llamada.


- [ OUT name ] : Nombre del nodo físico en que corre el proceso (cadena de caracteres, cuyo tamaño
debe ser al menos igual a MPI_MAX_PROCESSOR_NAME).
- [ OUT resultlen ] : Número de elementos que hay en el buffer de recepción.

101
Ejemplo básico:
#include <mpi.h>

main(int argc, char *argv[]) {


int idProceso;
int nProcesos;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &idProceso);
MPI_Comm_size(MPI_COMM_WORLD, &nProcesos);
if (idProceso == 0) {
printf("Proceso maestro : %d \n", idProceso);
}
else {
printf("Proceso esclavo : %d \n", idProceso);
}
MPI_Finalize();
}

Argumentos de los mensajes:

La mayoría de las funciones de mensajería tienen los siguientes argumentos:

- buff : La variable (puntero) que se va a enviar.

- count : El número de variables que se envían. Si se envía una única variable, valdrá 1 pero, si estamos
pasando un vector, valdrá el número total de elementos de dicho vector. Por ejemplo, para enviar un
vector de 4x5 especificaremos un valor de 20.

- MPI_TYPE : El tipo de la variable que estamos enviando.

- Source/dest : Es el identificador (número) del proceso de donde llega / a donde va el mensaje.

- tag : Etiqueta del mensaje. Sirve para verificar que el mensaje que se recibe es el que estaba esperando.
Se trata de un número entero al que le podemos asignar cualquier valor.

- comm : Este es el grupo al cual va el mensaje. Es típico para programas pequeños usar el grupo
MPI_COMM_WORLD. En programas grandes y complejos, los procesos pueden ser divididos en diferentes
grupos para mejorar la velocidad de las conexiones y las transferencias.

- status: Chequea si el mensaje fue correcto.

102
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Funciones de comunicación punto a punto:

Incluyen funciones para realizar llamadas de envío y recepción entre dos procesos. Existen dos categorías básicas
de estas funciones.

- Bloqueantes: [MPI_Send y MPI_Recv] Retornan cuando el envío o la recepción se ha completado.

- No bloqueantes: [MPI_ISend y MPI_IRecv] Retornan inmediatamente y queda al arbitrio del programador


chequear o no si las llamadas se han completado.

No obstante, para verificar si las llamadas no bloqueantes se han completado se emplean los test: MPI_TEST,
MPI_WAIT:

int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)

- [ IN buf ] Dirección inicial del búfer de envío.


- [ IN count ] Número de elementos en el búfer de envío.
- [ IN datatype ] Tipo de dato de cada elemento en el búfer de envío.
- [ IN dest ] Identificador (rango) del proceso destino.
- [ IN tag ] Etiqueta del mensaje.
- [ IN comm ] Comunicador.

Se trata de una operación de envío básico bloqueante. La rutina retorna cuando el buffer de la aplicación está
libre y puede ser reutilizado.

Este comando permite el paso de cualquier tipo de variable, incluso un vector grande, a cualquier grupo de
procesos. El mensaje contiene count elementos de un tipo de dato especificado por el datatype, comienza en la
dirección de memoria buf, usa la etiqueta de mensaje tag, lo envía al proceso cuyo identificador es dest, en el
comunicador comm.

int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm,
MPI_Status *status)

- [ OUT buf ] Dirección inicial del buffer de los datos recibidos.


- [ IN count ] Número de elementos en el buffer de recepción.
- [ IN datatype ] Tipo de dato de cada elemento en el buffer de envío.
- [ IN source ] Identificador (rango) del proceso emisor o MPI_ANY_SOURCE.
- [ IN tag ] Etiqueta del mensaje o MPI_ANY_TAG.
- [ IN comm ] Comunicador.
- [ OUT status ] Información de estado de completitud del proceso (estructura con tres campos que
contienen el rango del proceso fuente, la etiqueta y el código de error producido).

Bloquea un proceso hasta que recibe un mensaje del proceso cuyo rango es source, en el comunicador comm, con
etiqueta de mensaje tag.

Se pueden usar los comodines MPI_ANY_SOURCE y MPI_ANY_TAG para recibir mensajes. En ese caso, el status de
salida puede ser usado para conocer la fuente y etiqueta del mensaje recibido.

El mensaje recibido se guarda en un buffer consistente en un espacio de memoria que contiene count elementos
consecutivos del tipo especificado por datatype, comenzando en la dirección de memoria buf.

La longitud del mensaje recibido debe ser menor o igual que la longitud del buffer de recepción disponible.
103
Ejemplo de paso de mensajes (saludo en anillo):

#include <stdio.h>
#include <string.h>
#include <mpi.h>

main(int argc, char *argv[]) {

int idProceso; // identificador (rango) de mi proceso


int nProcesos; // número de procesos
int fuente; // identificador (rango) del emisor
int dest; // identificador (rango) del destinatario
int etiqueta = 0; // etiqueta para los mensajes
char mensaje[100]; // almacenamiento del mensaje

MPI_Status status;
MPI_Init(&argc, &argv);

MPI_Comm_rank(MPI_COMM_WORLD, &idProceso);
MPI_Comm_size(MPI_COMM_WORLD, &nProcesos);

if (idProceso != 0)
{ // Esclavo: Genera un mensaje y lo envía al maestro (0)
sprintf(mensaje, "Saludos desde el proceso esclavo %d!", idProceso);
dest = 0;
MPI_Send(mensaje, strlen(mensaje) + 1, MPI_CHAR, dest, etiqueta, MPI_COMM_WORLD);
}
else
{ // Maestro: Recibe un mensaje desde cada esclavo.
for (fuente = 1; fuente < nProcesos; fuente++) {
MPI_Recv(mensaje, 100, MPI_CHAR, fuente, etiqueta, MPI_COMM_WORLD, &status);
printf("%s\n", mensaje);
}
}

MPI_Finalize();

Nota 1: En la función de envío MPI_Send se ha indicado strlen(mensaje) + 1 para que el carácter de fin de
cadena ’\0’ pueda también ser enviado.

Nota 2: Hay que tener cuidado con los “Deadlock”, esto es, ejecutar una función de recibir sin haber enviado.

104
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Comunicación colectiva.

Las comunicaciones colectivas involucran a todos los procesos dentro del foco del comunicador. Todos los
procesos son por defecto miembros de un comunicador. El comunicador por defecto es: MPI_COMM_WORLD.

Existen tres tipos de operaciones colectivas:

- Sincronización. Los procesos esperan hasta que todos los miembros del grupo han alcanzado el punto
de sincronización.

- Movimiento colectivo de datos. Broadcast, Scatter/Gather, All_to_All, …

- Computación colectiva (reduction). Es similar que en OpenMP pero soporta más operaciones (min, max,
add, multiply, etc.). Un miembro del grupo recoge los datos del resto de miembros y realiza la operación
especificada sobre los datos.

Es importante destacar que las operaciones colectivas son bloqueantes por sí mismas, así como que se trabaja
exclusivamente con tipos de datos definidos para MPI y no con los tipos propios de C.

o Barrier
int MPI_Barrier(MPI_Comm comm)

Establece una barrera de sincronización en un grupo. Cada proceso, cuando ejecuta la función MPI_Barrier,
se bloquea hasta que todos los procesos del grupo ejecuten la misma función. No se usa mucho, aunque sí
en mensajes bloqueantes.

o Comunicación colectiva: Broadcast


int MPI_Bcast ( void *buff
, int count
, MPI_Datatype datatype
, int root
, MPI_Comm comm
)

Envía un mensaje desde un proceso al resto de


procesos del grupo.

buff : Nombre de la variable donde se almacenan


los datos a ser enviados por broadcast. Cuando la
función retorna, todos los procesos tienen el
mismo valor colocado en buff.

root : Este es el proceso el cual tiene el valor a ser


compartido con el resto de procesos. Si buff=10
en el proceso 3 y ponemos root=3, esto provocará
que buff=10 en todos los procesos.

105
o Comunicación colectiva: Scatter
int MPI_Scatter ( void *sendbuf
, int sendcnt
, MPI_Datatype sendtype
, void *recvbuf
, int recvcnt
, MPI_Datatype recvtype
, int root
, MPI_Comm comm
)

Distribuye distintos mensajes de un simple proceso


fuente a cada una de los procesos restantes del
grupo.

o Comunicación colectiva: Gather


int MPI_Gather ( void *sendbuf
, int sendcnt
, MPI_Datatype sendtype
, void *recvbuf
, int recvcnt
, MPI_Datatype recvtype
, int root
, MPI_Comm comm
)

Junta los distintos mensajes de cada proceso en el


grupo en un único proceso destino. Esta operación
es la contraria de MPI_Scatter.

106
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

o Comunicación colectiva: Allgather


int MPI_Allgather ( void *sendbuf
, int sendcnt
, MPI_Datatype sendtype
, void *recvbuf
, int recvcnt
, MPI_Datatype recvtype
, int root
, MPI_Comm comm
)

Concatena los datos de todos los procesos en un


grupo. Cada proceso del grupo realiza una
operación de broadcast uno-a-todos con el resto
del grupo.

o Comunicación colectiva: Alltoall


int MPI_AlltoAll ( void *sendbuf
, int sendcnt
, MPI_Datatype sendtype
, void *recvbuf
, int recvcnt
, MPI_Datatype recvtype
, int root
, MPI_Comm comm
)

Cada proceso de un grupo realiza una operación


scatter, enviando un mensaje distinto a todos
los procesos del grupo ordenados por el índice.
Realiza la traspuesta de la matriz.

107
o Comunicación colectiva: Reduce

Frecuentemente hacemos cálculos en los procesos que necesitamos recoger o reducir los datos obtenidos
sobre uno o todos los procesos. MPI dispone de rutinas que permiten realizar estas reducciones.
int MPI_Reduce ( void *sendbuf
, void *recvbuf
, int recvcnt
, int count
, MPI_Datatype datatype
, MPI_Op op
, int root
, MPI_Comm comm
)

Donde:

sendbuf : La variable a ser colectada de todos los procesos.


recbuf : La variable donde se guardará el resultado de la operación indicada por MPI_OP.
MPI_OP : Función a realizar sobre los valores colectados.
root : El único proceso que contendrá el resultado final almacenado en recbuf.

Si cada proceso en nuestro trabajo ha calculado un valor privado para alguna variable, var, con el mismo
nombre en todos los procesos, la rutina MPI_REDUCE reducirá todos estos valores, de acuerdo a alguna
operación, y a continuación guardará el resultado en una variable sobre uno de los procesos.

Operador MPI Descripción del cálculo


MPI_MAX Máximo
MPI_MIN Mínimo
MPI_PROD Producto
MPI_SUM Suma
MPI_LAND AND Lógico
MPI_LOR OR Lógico
MPI_LXOR EXCLUSIVE OR Lógico
MPI_BAND AND Bit-a-bit
MPI_BOR OR Bit-a-bit
MPI_BXOR EXCLUSIVE OR Bit-a-bit
MPI_MAXLOC Valor máximo local
MPI_MINLOC Valor mínimo local

o Comunicación colectiva: Allreduce

Podemos realizar una reducción y enviar el resultado a todos los procesos. Esta operación es equivalente a
un MPI_Reduce seguido de un MPI_Bcast. La única diferencia entre esta llamada y MPI_REDUCE es que no existe
el campo root.
int MPI_Allreduce( void *sendbuf
, void *recvbuf
, int count
, MPI_Datatype datatype
, MPI_Op op
, MPI_Comm comm
)

108
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejemplo del cálculo de la media aritmética usando Comunicación Colectiva:


// Suponemos búferes ya reservados
// El maestro (0) deberá crear búferes para los resultados parciales

// Scatter desde el maestro (0) al resto


MPI_Scatter(nums, eltoXproc, MPI_FLOAT, parcial_nums, eltoXproc, MPI_FLOAT, 0, MPI_COMM_WORLD);

// Función que computa la media parcial


float parcial_avg = calcular_avg(parcial_nums, eltoXproc);

// Gather hacia el maestro


MPI_Gather(&local_avg, 1, MPI_FLOAT, parcial_avgs, 1, MPI_FLOAT, 0, MPI_COMM_WORLD);

// Computa la media total


if (id_proceso == 0)
float avg = calcular_avg(parcial_avgs, num_process);

Código completo:

#include <stdio.h>
#include <mpi.h>

main (int argc, char *argv)


{

int idProceso, nProcesos, i, a=5;


double x[N], y[N];
int *xbuff, *ybuff;

MPI_Init(&argc, &argv);
MPI_Comm_idProceso(MPI_COMM_WORLD, &idProceso);
MPI_Comm_nProcesos(MPI_COMM_WORLD, &nProcesos);

/* El proceso maestro inicializa los vectores */


if (idProceso == 0) {
for (i = 0; i < N; i++){
x[i] = rand();
y[i] = rand();
}
}

/* El proceso maestro envía los vectores a todos los procesos esclavos,


que esperan recibirlos */
MPI_Scatter(x, N, MPI_DOUBLE, xbuff, idProceso, MPI_DOUBLE, MPI_COMM_WORLD);
MPI_Scatter(y, N, MPI_DOUBLE, ybuff, idProceso, MPI_DOUBLE, MPI_COMM_WORLD);

/* Ahora puede empezar el bucle en cada proceso, accediendo sólo a su sección de datos */
for(i = idProceso*(N/nProcesos); i < (idProceso+1)*(N/nProcesos); i++){
ybuff[i] = a * xbuff[i] + ybuff[i];
}

/* Recolecta en el proceso maestro los resultados del vector y */


MPI_Gather(y, N, MPI_DOUBLE, ybuff, idProceso, MPI_DOUBLE, MPI_COMM_WORLD);

if (idProceso == 0){
for (i = 0; i < N; i++)
printf("[%d] %f\n", idProceso, y[i]);

MPI_Finalize();
}

109
Ejemplo cálculo del número π:

Cálculo del número π mediante integración numérica, teniendo en cuenta que la integral en el intervalo [0, 1]
de la derivada del arco tangente de x es π/4.
1
𝑎𝑟𝑐𝑡𝑔′(𝑥) =
1 + 𝑥2 1
1 𝜋
𝜋 ⇒ ∫ = 𝑎𝑟𝑐𝑡𝑔(𝑥)10 = −0
𝑎𝑟𝑐𝑡𝑔(1) = 0 1+𝑥 2 4
4
𝑎𝑟𝑐𝑡𝑔(0) =0 }

Vamos a basarnos en una versión secuencial. En ella se aproxima el área en cada subintervalo utilizando
rectángulos en los que la altura es el valor de la derivada del arco tangente en el punto medio.

Asignamos tareas a los procesos.

 Suponemos que tenemos un clúster Beowulf, formado por 8 PCs. En cada procesador se ejecutará un
proceso y, la carga de trabajo se distribuirá por igual entre los procesadores. Realizaremos una
planificación estática de las tareas.

 Escribimos el código paralelo usando MPI (paso de mensajes).

 Utilizaremos paralelismo de datos y, partiendo de la versión secuencial, obtenemos el código paralelo.

110
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Crear y terminar procesos:

 MPI_Init(): Crea el proceso que lo ejecuta dentro del mundo MPI, es decir, dentro del grupo de procesos
denominado MPI_COMM_WORLD. Una vez que se ejecuta esta función se pueden utilizar el resto de
funciones MPI.

 MPI_Finalice(): Se debe llamar antes de que un proceso creado en MPI acabe su ejecución.

 MPI_Comm_size(MPI_COMM_WORLD, &nproc): Con esta función se pregunta a MPI el número de procesos


creados en el grupo MPI_COMM_WORLD, se devuelve en nproc.

 MPI_Comm_rank(MPI_COMM_WORLD, &iproc): Con esta función se devuelve al proceso su identificador,


iproc, dentro del grupo.

Asignar tareas a procesos y localizar paralelismo:

- Se reparten las iteraciones del bucle entre los diferentes procesos MPI de forma explícita.

- Se utiliza un turno rotatorio.

Comunicación y sincronización:

- Los procesos se comunican y sincronizan para sumar las áreas parciales calculadas por los procesos.

- Usamos MPI_Reduce. Esta es una operación cooperativa realizada entre todos los miembros del
comunicador y se obtiene un único resultado final.

- Los procesos envían al proceso 0 el contenido de su variable local sum, los contenidos de sum se suman
y se guardan en el proceso 0 en la variable tsum.

Ejecución y programación SPMD (Single-Program Multiple-Data)

El comando de ejecución depende de la implementación concreta de MPI que se use, pero la esencia es la misma:

- Se coloca una copia del ejecutable en cada procesador.


- Cada procesador comienza la ejecución de su copia del ejecutable.
- Procesos diferentes pueden ejecutar instrucciones distintas incluyendo en el programa una bifurcación
condicional basada en el valor del rango del propio.

La programación SPMD, que no hay que confundirla con la programación SIMD, es la forma más general de
programación MIMD, cada proceso ejecuta un programa diferente, pero puede conseguirse lo mismo con un
único programa que incluya bifurcaciones según el identificador (rango) del proceso.

111
Caracterización de las aplicaciones.

Afirmamos que un programa es escalable si:

 Crece linealmente (o más) al crecer el número de procesadores p.

 Crece a un ritmo inferior, no es escalable, pero al aumentar p, el


programa se “degrada”.

Resulta difícil analizar si una aplicación es paralelizable o no puesto que depende de muchos factores:

- Cómo se escriba el algoritmo: Secuencial o Paralelizable.


- Distribución de los datos: Si todos están en un mismo
nodo, distribuidos o el paso es mínimo.
- Sincronización eficiente: Si hay que sincronizar
constantemente se pierde eficiencia.
- Ocultación de acceso a memoria.
- Acceso a la red.

Aplicación Escala de Escala de Computación vs


[p: Procesos, n: Tamaño datos] computación comunicación Comunicación
𝑛 ∙ log(𝑛) 𝑛
FFT (Tabla de Furier) log(𝑛)
𝑝 𝑝
𝑛 √𝑛 √𝑛
LU (Factorización de Matrices)
𝑝 √𝑝 √𝑝
𝑛 √𝑛 √𝑛
Ocean (Elementos Finitos 2D)
𝑝 √𝑝 √𝑝

Tiempo de comunicación.
𝑇
𝑡𝑚𝑒𝑛𝑠 = 𝑡𝑠 + 𝑡𝑡𝑟𝑎𝑛𝑠𝑓𝑒𝑟 = 𝑡𝑠 + (𝐿 ∙ 𝑡𝑙𝑎𝑡𝑒𝑛𝑐𝑖𝑎 + )
𝐴𝐵𝑡𝑟𝑎𝑛𝑠𝑚𝑖𝑠𝑖ó𝑛

ts: Tiempo de preparación del mensaje en el emisor (>> que tr el receptor)


L: Distancia entre nodos.
tlatencia : Tiempo de latencia para el primer bit.
T: Tamaño en Bytes.

Evaluación del rendimiento.

MPI dispone de una función para medir los tiempos de ejecución con la que se puede evaluar el rendimiento de
los programas. Al ser una función del API estándar MPI, tiene como ventaja su portabilidad.

double MPI_Wtime(void): Devuelve un número de segundos en punto flotante, que representan una referencia
de tiempo (wall-clock time).

Para medir la duración de un proceso, se puede llamar a MPI_Wtime justo antes de que comience y justo después
de que termine. Posteriormente se calcula la diferencia entre los dos tiempos obtenidos.
112
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

3.5 Ejercicios.

¿Qué es un patrón de comunicaciones?

Desde un punto de visto lógico, se refiere a las comunicaciones que se establecen en la red y es
independiente de la aplicación. Por tanto se refiere al conexionado lógico de los nodos, independientemente
de la propia topología de la red.

Desde el punto de vista de la topología de una red, es la implementación física de la red (Anillo, Árbol,
Estrella,…), por lo que el diseño de la aplicación es independiente del diseño físico de la red, pero algunas
topologías favorecen dichas aplicaciones.

El diseño lo estudiaremos en el centro (bisectriz) de la red; esto es, dividimos físicamente la red y veremos cuál
es el comportamiento de la aplicación, si el ancho de banda en dicha bisectriz física es mayor o igual al ancho de
banda del programa.

Empecemos por el ancho de banda física partiendo de que todos los enlaces son iguales, tendremos entonces:
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 = 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 ∙ 𝐴𝐵𝐸𝑛𝑙𝑎𝑐𝑒
𝐹í𝑠𝑖𝑐𝑎

donde AB Bisección se corresponde con el número de enlaces que tenemos que cortar para obtener dos subredes
lo más parecidas posible. Estamos suponiendo que todos los anchos de banda son iguales. En caso de que los
AB Bisección sean diferentes, entonces tomaríamos:
𝑁ú𝑚𝑒𝑟𝑜 𝐴𝐵−1

∑ 𝐴𝐵𝐸𝑛𝑙𝑎𝑐𝑒𝑖
𝑖=0

Posteriormente, estudiaremos qué ancho de banda requiere nuestro programa de la siguiente forma:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝑁º𝑁𝑜𝑑𝑜𝑠 ∙ %𝑀𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 ∙ 𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟


𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜

113
Ejemplo.

Supongamos 6 nodos conectados en anillo, donde cada uno de ellos emite 3 mensajes, dos de ellos a los dos
siguientes y uno al anterior.

Veamos gráficamente el caso del nodo 6 enviando sus 3 mensajes, de los cuales sólo 1 atraviesa la bisectriz.

6 1

5 2

4 3

Tenemos entonces:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝑁º𝑁𝑜𝑑𝑜𝑠 ∙ %𝑀𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 ∙ 𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟


𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 ⏟ ⏟ 𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ⏟ 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜
(𝑎) (𝑏) (𝑐)

(a) Número total de nodos = 6.

𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑞𝑢𝑒 𝑎𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 1 𝑚𝑠𝑔 [𝑛𝑜𝑑𝑜 6 → 𝑛𝑜𝑑𝑜 5] + 1 𝑚𝑠𝑔 [𝑛𝑜𝑑𝑜 2 → 𝑛𝑜𝑑𝑜 3] 2
(b) 𝑁ú𝑚𝑒𝑟𝑜 𝑡𝑜𝑡𝑎𝑙 𝑑𝑒 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑔𝑒𝑛𝑒𝑟𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜
= 𝑐𝑎𝑑𝑎 𝑢𝑛𝑜 𝑑𝑒 𝑙𝑜𝑠 6 𝑛𝑜𝑑𝑜𝑠 𝑔𝑒𝑛𝑒𝑟𝑎 3 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠
= 18

(c) Ancho de banda requerido por la ejecución en 1 nodo se calcula de la siguiente forma:
𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝐶𝑃𝑈 𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟 = ∙ %[ ∙ %[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑒𝑛𝑠𝑎𝑗𝑒
𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

Puede ocurrir que cuando el microprocesador acceda a la red se bloquee, por lo que:
CPI TOTAL = CPIT TOTAL + CPI Bloqueo . Pero si el microprocesador no se bloqueara, entonces CPI TOTAL = CPI IDEAL.
𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = % [ ∙ %[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ ⏟
𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑
⏟ 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠
𝑇𝑖𝑒𝑚𝑝𝑜 𝑑𝑒 𝑎𝑐𝑐𝑒𝑠𝑜 𝑟𝑒𝑎𝑙
𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎 𝑑𝑒 𝑎𝑐𝑐𝑒𝑠𝑜 𝑎 𝑙𝑎 𝑟𝑒𝑑

Donde 𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑 ≅ 2 ∙ 𝑙 ∙ 𝑇ℎ𝑒𝑎𝑑

Donde: 𝑙 = Número de enlaces medio para ir desde el nodo emisor de


los mensajes a los nodos receptores de ellos.
T head = Tiempo requerido para el envío de la cabecera.

El TamañoMensaje consiste en la petición de un dato y su correspondiente respuesta; es decir, en realizar una


Petición más obtener una respuesta. Habitualmente se nos proporcionan los datos de cabecera y línea.
Se calcula como: Cabecera + (Cabecera + línea).
114
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio Pregunta 3 del Examen 02/09/1999

Un departamento de investigación en fluidos quiere comprar un multiprocesador NUMA para ejecutar


aplicaciones donde la comunicación entre los elementos finitos del fluido tridimensional simulado es solamente
adyacente (similar al programa Ocean pero en tres dimensiones).

Los microprocesadores RISC que se usarán tienen una frecuencia de CPU de 300 MHz. Tras simular los programas
en tales microprocesadores, se obtienen las siguientes estadísticas: %Accesos remotos = 10%, %LD/ST = 25%.

El código siempre permanece en el caché de instrucciones. Tamaño medio de un mensaje = 100 Bytes.

Se está barajando la posibilidad de conectar los nodos con alguna de las topologías de switches distribuidos
siguientes: Cubo de 8 nodos y Malla 2D rectangular de 4x2 nodos.

Ambas redes usan enlaces de 125 MBytes/segundo cada una. El mapeo de los nodos con los elementos finitos
se muestra en la siguiente figura:

Cubo (3D) Malla (2D)

Se pide:

a) Calcular el Miss Rate máximo del caché de datos para cada topología, para que se soporte el tráfico en la
bisectriz de la aplicación. Suponer que el CPI (con todo tipo de bloqueos incluidos) = 0’8 ciclos/instrucción
para ambas topologías.

b) Tómese ahora Miss Rate (Caché datos) = 6% y CPI (suponiendo red ideal) = 0’6 ciclos/instrucción para ambas
topologías. Calcular la relación de tiempos de ejecución entre el nodo 7 del Cubo y el mismo nodo de la
Malla al ejecutar N instrucciones. Suponer que el tiempo de acceso medio a la red se puede aproximar por
la fórmula (2 ∙ l ∙ th ) + ttransf , donde l es el número medio de enlaces exteriores entre el procesador origen y
el de destino, th es 0’5 microsegundos y ttransf es el tiempo en transferir los 100 Bytes del mensaje.

115
Solución:

Apartado a)

 Cálculo del ancho de banda físico en la bisectriz:

Sabiendo que:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 = 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 ∙ 𝐴𝐵𝐸𝑛𝑙𝑎𝑐𝑒


𝐹í𝑠𝑖𝑐𝑎

Caso : Cubo 3D
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 = 𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑡𝑟𝑎𝑚𝑜𝑠 𝑐𝑜𝑟𝑡𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 4
𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐸𝑛𝑙𝑎𝑐𝑒 = 125
𝑠𝑒𝑔𝑢𝑛𝑑𝑜

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝑀𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠


𝐹í𝑠𝑖𝑐𝑎 = 4 ∙ 125 = 500
𝐶𝑢𝑏𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

Caso : Malla 2D

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 = 𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑡𝑟𝑎𝑚𝑜𝑠 𝑐𝑜𝑟𝑡𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 2


𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐸𝑛𝑙𝑎𝑐𝑒 = 125
𝑠𝑒𝑔𝑢𝑛𝑑𝑜

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝑀𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠


𝐹í𝑠𝑖𝑐𝑎 = 2 ∙ 125 = 250
𝑀𝑎𝑙𝑙𝑎 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

116
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

 Cálculo del ancho de banda del programa en la bisectriz (es el mismo para ambos casos):

Sabemos que:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝑁º𝑁𝑜𝑑𝑜𝑠 ∙ %𝑀𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 ∙ 𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟


𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 ⏟ ⏟ 𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ⏟ 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜
(𝑎) (𝑏) (𝑐)

(a) El número total de nodos es:

Nº Nodos = 8

(b) Número de mensajes que atraviesan la bisectriz:


𝐶𝑎𝑑𝑎 𝑢𝑛𝑜 𝑑𝑒 𝑙𝑜𝑠 8 𝑛𝑜𝑑𝑜𝑠 𝑡𝑖𝑒𝑛𝑒 1 𝑚𝑒𝑛𝑠𝑎𝑗𝑒 𝑞𝑢𝑒 𝑎𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎 8 ∙ 1 1
%𝑀𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 = = =
𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝐶𝑎𝑑𝑎 𝑢𝑛𝑜 𝑑𝑒 𝑙𝑜𝑠 8 𝑛𝑜𝑑𝑜𝑠 𝑔𝑒𝑛𝑒𝑟𝑎 3 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 8 ∙ 3 3

(c) Para ambos casos, el AB generado por cada nodo es el mismo.


𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝐶𝑃𝑈 𝐿𝑜𝑎𝑑𝑠 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟 = ∙ %[ ] ∙ %[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑒𝑛𝑠𝑎𝑗𝑒
𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

300 𝑀𝐻𝑧
= ∙ 0′ 25 ∙ 0′ 10 ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 100 𝐵𝑦𝑡𝑒𝑠
0′ 8 𝑐/𝑖

𝑀𝐵𝑦𝑡𝑒𝑠
= 937′ 5 ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒
𝑠𝑒𝑔𝑢𝑛𝑑𝑜

 Para poder determinar el valor de 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 podemos igualar las fórmulas: 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧
𝐹í𝑠𝑖𝑐𝑎 𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎

𝑀𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠 8
𝐶𝑎𝑠𝑜 𝐶𝑢𝑏𝑜 ∶ (500 = 937′ 5 ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ) ⇒ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 =
𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 15

𝑀𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠 4
𝐶𝑎𝑠𝑜 𝑀𝑎𝑙𝑙𝑎 ∶ (250 = 937′ 5 ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ) ⇒ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 =
𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 15

Adicionalmente, sabremos que:


1 8 𝑀𝐵𝑦𝑡𝑒𝑠 1 4 𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 8 ∙ ∙ = 1′ 42̂ 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 8 ∙ ∙ = 0′ 71̂
𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 3 15 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 3 15 𝑠𝑒𝑔𝑢𝑛𝑑𝑜
𝐶𝑢𝑏𝑜 𝑀𝑎𝑙𝑙𝑎

117
Apartado b)

Determinemos inicialmente el tiempo de acceso a la red:

𝑇𝐴𝑐𝑐.𝑅𝑒𝑑 ≅ (2 ∙ 𝑙 ∙ 𝑇ℎ𝑒𝑎𝑑 ) + 𝑇𝑡𝑟𝑎𝑛𝑠𝑓

Vamos a obtener el valor de 𝑙 , el número de enlaces medio para ir desde el nodo emisor de los
mensajes al nodo receptor de ellos, estudiando qué sucede en algún nodo de cada topología:
(1 𝑡𝑟𝑎𝑚𝑜 ∙ 3 𝑚𝑠𝑔)
𝐶𝑎𝑠𝑜 𝐶𝑢𝑏𝑜: 𝑙= =1
3 𝑚𝑠𝑔
(1 𝑡𝑟𝑎𝑚𝑜 ∙ 2 𝑚𝑠𝑔) + (3 𝑡𝑟𝑎𝑚𝑜𝑠 ∙ 1 𝑚𝑠𝑔) 5
𝐶𝑎𝑠𝑜 𝑀𝑎𝑙𝑙𝑎: 𝑙= =
3 𝑚𝑠𝑔 3
Ahora podremos determinar el tiempo de acceso a la red de cualquier nodo de cada topología:
𝑀𝐻𝑧
𝑇ℎ𝑒𝑎𝑑 = 0′ 5 𝜇𝑠 → 0′ 5 𝜇𝑠 ∙ 300 = 150 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈
𝑠𝑒𝑔𝑢𝑛𝑑𝑜𝑠
100 𝐵𝑦𝑡𝑒𝑠 𝑀𝐻𝑧
𝑇𝑡𝑟𝑎𝑛𝑠𝑓 = = 0′ 8 𝜇𝑠 → 0′ 8 𝜇𝑠 ∙ 300 = 240 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈
𝑀𝐵𝑦𝑡𝑒𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜𝑠
125 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

𝐶𝑎𝑠𝑜 𝐶𝑢𝑏𝑜: 𝑇𝐴𝑐𝑐.𝑅𝑒𝑑 ≅ (2 ∙ 1 ∙ 150) + 240 = 540 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈


5
𝐶𝑎𝑠𝑜 𝑀𝑎𝑙𝑙𝑎: 𝑇𝐴𝑐𝑐.𝑅𝑒𝑑 ≅ (2 ∙ ∙ 150) + 240 = 740 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈
3

Determinemos ahora cuál es el tiempo de ejecución del programa en el algún nodo de cada topología:
𝑇𝐸𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛 = 𝑁º𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 ∙ 𝐶𝑃𝐼 ∙ 𝑇

Si relacionamos los tiempos de ejecución de cualquier nodo para ambas topologías tendremos:
𝑇𝐸𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛(𝑀𝑎𝑙𝑙𝑎) 𝑁º𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 ∙ 𝐶𝑃𝐼(𝑀𝑎𝑙𝑙𝑎) ∙ 𝑇
𝑅𝑒𝑙𝑎𝑐𝑖ó𝑛 = =
𝑇𝐸𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛(𝐶𝑢𝑏𝑜) 𝑁º𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 ∙ 𝐶𝑃𝐼(𝐶𝑢𝑏𝑜) ∙ 𝑇

𝐶𝑃𝐼(𝑀𝑎𝑙𝑙𝑎) = 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 + 𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = 0′ 6 + 1′ 11 = 1′71

𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = 0′ 25 ∙ 0′ 10 ∙ 0′ 06 ∙ 740 = 1′11

𝐶𝑃𝐼(𝐶𝑢𝑏𝑜) = 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 + 𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = 0′ 6 + 0′ 81 = 1′41

𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = 0′ 25 ∙ 0′ 10 ∙ 0′ 06 ∙ 540 = 0′81

𝐶𝑃𝐼(𝑀𝑎𝑙𝑙𝑎) 1′71
𝑅𝑒𝑙𝑎𝑐𝑖ó𝑛 = = = 𝟏′𝟐𝟏
𝐶𝑃𝐼(𝐶𝑢𝑏𝑜) 1′41

Concluimos con que la topología Cubo es un 21% más rápido que la topología Malla. Si hubiéramos
considerado los nodos centrales de la Malla hubiera arrojado los mismos resultados, pues se atraviesa
el mismo número de nodos.

118
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio Pregunta 2 del Examen 17/06/2002

Disponemos de un sistema multiprocesador tipo NUMA, con 64 procesadores (con una CPU = 500 MHz y con
CPIBase = 0’4 ciclos/instc.). La red de interconexión es una malla 2D cuadrada, siendo su frecuencia de 100 MHz y
el ancho del enlace de 16 bits. Pretendemos ejecutar un programa cuyo patrón de comunicaciones es, adyacente
con todos los nodos de su entorno (incluidos los que se encuentran en su diagonal). De la ejecución de dicho
programa hemos extraído los siguientes datos: %LD/ST = 20%, %Accesos locales = 85%, Missrate = 3%, Tamaño de
la línea = 128 Bytes, Tamaño cabecera = 32Bytes.

a) Calcular el 𝑡𝑎𝑐𝑐.𝑅𝑒𝑑 (en ciclos de CPU), si este se puede aproximar a 2 ∙ 𝑙 ∙ th , donde 𝑙 es el número medio
de nodos entre el procesador origen y el destino, y th = 20 ciclos de red.

b) ¿Soportaría la configuración anterior el ancho de banda generado en la bisectriz por el programa?


Demostrarlo.

Solución:

Datos conocidos:
𝑁º𝑃𝑟𝑜𝑐𝑒𝑠𝑎𝑑𝑜𝑟𝑒𝑠 = 64 𝐸𝑛𝑙𝑎𝑐𝑒 = 16 𝑏𝑖𝑡𝑠 (2 𝐵𝑦𝑡𝑒𝑠)
𝐶𝑃𝑈𝑝𝑟𝑜𝑐𝑒𝑠𝑎𝑑𝑜𝑟 = 500 𝑀𝐻𝑧 𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎 = 100 𝑀𝐻𝑧
𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 = 0′ 4 𝑐/𝑖 𝑇𝑜𝑝𝑜𝑙𝑜𝑔í𝑎 = 𝑀𝑎𝑙𝑙𝑎 2𝐷 𝑐𝑢𝑎𝑑𝑟𝑎𝑑𝑎
% [𝐿𝑜𝑎𝑑/𝑆𝑡𝑜𝑟𝑒] = 20 % 𝑇𝑎𝑚𝑎ñ𝑜𝐿í𝑛𝑒𝑎 = 128 𝐵𝑦𝑡𝑒𝑠
% 𝐴𝑐𝑐𝑒𝑠𝑜𝑠𝑙𝑜𝑐𝑎𝑙𝑒𝑠 = 85 % 𝑇𝑎𝑚𝑎ñ𝑜𝐶𝑎𝑏𝑒𝑐𝑒𝑟𝑎 = 32 𝐵𝑦𝑡𝑒𝑠
𝑀𝑖𝑠𝑠𝑅𝑎𝑡𝑒 = 3%

Patrón de comunicaciones: Adyacentes a todos los nodos incluidos los de su diagonal.

119
Apartado a)

𝑇𝑎𝑐𝑐.𝑅𝑒𝑑 ≅ 2 ∙ 𝑙 ∙ 𝑇ℎ𝑒𝑎𝑑

𝑇𝑎𝑐𝑐.𝑅𝑒𝑑 ≅ 2 ∙ 1′ 45 ∙ 20 𝑐𝑖𝑐𝑙𝑜𝑠 𝑑𝑒 𝑟𝑒𝑑 = 58 𝑐𝑖𝑐𝑙𝑜𝑠 𝑑𝑒 𝑟𝑒𝑑

Donde:

Como hay 3 tipos de nodos diferentes (4 esquenas, 24 laterales y 36 centrales), calculamos los 𝑙
de cada uno de estos tipos y determinamos, posteriormente, su valor medio; esto es:

(𝑙𝑛𝑜𝑑𝑜𝑠 ∙ 4) + (𝑙𝑛𝑜𝑑𝑜𝑠 ∙ 24) + (𝑙𝑛𝑜𝑑𝑜𝑠 ∙ 36)


𝑒𝑠𝑞𝑢𝑖𝑛𝑎𝑠 𝑙𝑎𝑡𝑒𝑟𝑎𝑙𝑒𝑠 𝑐𝑒𝑛𝑡𝑟𝑎𝑙𝑒𝑠
𝑙=
64 𝑛𝑜𝑑𝑜𝑠

(2 𝑚𝑠𝑔 𝑝𝑜𝑟 1 𝑡𝑟𝑎𝑚𝑜 ) + (1 𝑚𝑠𝑔 𝑝𝑜𝑟 2 𝑡𝑟𝑎𝑚𝑜𝑠) (2 ∙ 1) + (1 ∙ 2) 4


𝑙𝑛𝑜𝑑𝑜𝑠 = = =
𝑒𝑠𝑞𝑢𝑖𝑛𝑎𝑠 3 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 3 3

(3 𝑚𝑠𝑔 𝑝𝑜𝑟 1 𝑡𝑟𝑎𝑚𝑜 ) + (2 𝑚𝑠𝑔 𝑝𝑜𝑟 2 𝑡𝑟𝑎𝑚𝑜𝑠) (3 ∙ 1) + (3 ∙ 2) 7


𝑙𝑛𝑜𝑑𝑜𝑠 = = =
𝑙𝑎𝑡𝑒𝑟𝑎𝑙𝑒𝑠 5 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 5 5

(4 𝑚𝑠𝑔 𝑝𝑜𝑟 1 𝑡𝑟𝑎𝑚𝑜 ) + (4 𝑚𝑠𝑔 𝑝𝑜𝑟 2 𝑡𝑟𝑎𝑚𝑜𝑠) (4 ∙ 1) + (4 ∙ 2) 12


𝑙𝑛𝑜𝑑𝑜𝑠 = = =
𝑐𝑒𝑛𝑡𝑟𝑎𝑙𝑒𝑠 8 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 8 8

Por tanto, tendremos:


4 7 12
( ∙ 4) + ( ∙ 24) + ( ∙ 36)
𝑙 = 3 5 8 = 1′45
64 𝑛𝑜𝑑𝑜𝑠

Hemos obtenido el tiempo medio de acceso a la red expresado en ciclos de red, pero nos piden indicarlo
en ciclos de CPU, luego, sabiendo que Frecuencia red = 100 MHz y que Frecuencia CPU = 500 MHz :

100 ciclosred ≡ 500 ciclosCPU 58 ∙ 500


] 𝑥= = 290 ciclosCPU
58 ciclosred ≡ 𝑥 100

Concluimos indicando que el tiempo medio de acceso a la red es de 290 ciclos de CPU.

120
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Apartado b)

Veamos si soportaría la configuración dada el ancho de banda generado en la bisectriz por el programa.

 Determinamos primero el ancho de banda, en la bisectriz, requerido físicamente:


𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 ∙ 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧
𝐹í𝑠𝑖𝑐𝑜 𝐸𝑛𝑙𝑎𝑐𝑒

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 = 𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑒𝑛𝑙𝑎𝑐𝑒𝑠 𝑐𝑜𝑟𝑡𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 = 8


𝑀𝐵𝑦𝑡𝑒𝑠 16 𝑏𝑖𝑡𝑠 𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝑅𝑒𝑑 ∙ 𝐴𝑛𝑐ℎ𝑜𝑅𝑒𝑑 = 100 ∙ = 200
𝐸𝑛𝑙𝑎𝑐𝑒 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 8 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

𝑀𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 8 ∙ 200 = 1.600
𝐹í𝑠𝑖𝑐𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

 Determinamos después el ancho de banda, en la bisectriz, requerido por el programa:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝑁º𝑁𝑜𝑑𝑜𝑠 ∙ %𝑀𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 ∙ 𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟


𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 ⏟ ⏟ 𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ⏟ 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜
(𝑎) (𝑏) (𝑐)

(a) El número total de nodos = 64 nodos.

(b) El porcentaje medio de mensajes que atraviesan la bisectriz es de:

(4 𝑛𝑜𝑑𝑜𝑠 → 2 𝑚𝑠𝑔) + (12 𝑛𝑜𝑑𝑜𝑠 → 3 𝑚𝑠𝑔)


𝐿𝑎𝑡𝑒𝑟𝑎𝑙𝑒𝑠 𝐶𝑒𝑛𝑡𝑟𝑎𝑙𝑒𝑠
%𝑀𝑠𝑔 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 =
𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧
(4 𝑛𝑜𝑑𝑜𝑠 → 3 𝑚𝑠𝑔) + (24 𝑛𝑜𝑑𝑜𝑠 → 5 𝑚𝑠𝑔) + (36 𝑛𝑜𝑑𝑜𝑠 → 8 𝑚𝑠𝑔)
𝐸𝑠𝑞𝑢𝑖𝑛𝑎𝑠 𝐿𝑎𝑡𝑒𝑟𝑎𝑙𝑒𝑠 𝐶𝑒𝑛𝑡𝑟𝑎𝑙𝑒𝑠

(4 ∙ 2) + (12 ∙ 3) 44
= = 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠
(4 ∙ 3) + (24 ∙ 5) + (36 ∙ 8) 420
121
(c) El ancho de banda generado por cada nodo es de:

𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝐶𝑃𝑈 𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠


𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟 = ∙ %[ ∙ %[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑎𝑚𝑀𝑒𝑛𝑠𝑎𝑗𝑒
𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

500 𝑀𝐻𝑧 𝐵𝑦𝑡𝑒𝑠


= ∙ 0′ 20 ∙ 0′ 15 ∙ 0′ 03 ∙ 192 𝐵𝑦𝑡𝑒𝑠 = 216
′ 𝑐𝑖𝑐𝑙𝑜𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜
0 4 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

Donde:

- Deberemos tomar la decisión, pues no se indica en el enunciado acerca de si el procesador


se bloquea o no cuando se accede a la red. Tomaremos que no se produce bloqueo, por
lo que entonces no hay CPI Bloqueo y, consecuentemente:

CPI TOTAL = CPI Ideal = 0’4 ciclos/instrucción

- Nos aportan el %Accesos locales, pero necesitamos conocer el %Accesos remotos, luego:
𝐴𝑐𝑐𝑒𝑠𝑜𝑠 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
%[ ] = 100% − % [ ] = 100% − 85% = 15% → 0′15
𝑟𝑒𝑚𝑜𝑡𝑜𝑠 𝑙𝑜𝑐𝑎𝑙𝑒𝑠

- Por otro lado, determinamos el tamaño de los mensajes:

Tamaño Mensaje = Peticiones + Respuestas = Cabecera + (Cabecera + Línea)

Tamaño Mensaje = 32 Bytes + (32 Bytes + 128 Bytes) = 192 Bytes

Por tanto, tenemos:


44 𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = (64 𝑛𝑜𝑑𝑜𝑠) ∙ ( 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠) ∙ (216 ) ≅ 1.448
𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 420 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

Observamos que:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 > 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧


𝐹í𝑠𝑖𝑐𝑜 𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎
⏟ ⏟
𝑀𝐵 𝑀𝐵𝑦𝑡𝑒𝑠
1.600 1.448
𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

Como el ancho de banda físico soporta más carga que el requerido por la aplicación, podemos concluir
indicando que la red soporta la demanda de la aplicación.

122
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio Pregunta 3 del Examen 24/09/2005

Pretendemos estudiar cómo se adapta un sistema multiprocesador NUMA con 16 nodos (procesadores RISC de
500 MHz) y con una topología en forma de Hipercubo a dos patrones de comunicación distintos. El primer en un
patrón Adyacente-2D, teniendo en cuenta que todos los nodos se comunican con aquellos con los cuales tienen
conexión directa. Por otra parte, tenemos un patrón de comunicaciones del tipo Todos con Todos. Suponiendo
que la CPU se bloquea en los accesos a la red y que el tiempo de acceso lo podemos aproximar por
t acc.Red ≈ 2 ∗ 𝑙 ∗ 𝑡ℎ . ¿Soportaría la red en su bisectriz el tráfico generado por ambos patrones de comunicación?

Datos: th = 15 ciclos de red, CPI base = 0’4 ciclos/instrucción, %LD/ST = 18%, %Accesos remotos = 18%, Mr = 5%,
tamaño de línea = 128 Bytes, tamaño de cabecera = 8 Bytes. Enlaces de frecuencia 125 MHz y 16 bits de ancho.

Solución:

13 9 8 12

15 11 10 14

5 1 0 4

7 3 2 6

Calculamos el ancho de banda físico en la bisectriz que, para ambos patrones, es el mismo:
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 ∙ 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧
𝐹í𝑠𝑖𝑐𝑜 𝐸𝑛𝑙𝑎𝑐𝑒

= 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 ∙ (𝐴𝑛𝑐ℎ𝑜𝑅𝑒𝑑 ∙ 𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝑅𝑒𝑑 )

= 8 ∙ (2 𝐵𝑦𝑡𝑒𝑠 ∙ 125 𝑀𝐻𝑧)

𝑴𝑩𝒚𝒕𝒆𝒔
= 𝟐. 𝟎𝟎𝟎
𝒔𝒆𝒈𝒖𝒏𝒅𝒐

Calculamos ahora el ancho de banda requerido por el programa para cada patrón de comunicaciones:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝑁º𝑁𝑜𝑑𝑜𝑠 ∙ %𝑀𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 ∙ 𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟


𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 ⏟ ⏟ 𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ⏟ 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜
(𝑎) (𝑏) (𝑐)

123
Caso: Patrón adyacentes (conexión directa)

(a) El número total de nodos = 16 nodos.

(b) El porcentaje medio de mensajes que atraviesan la bisectriz es de:


𝑙𝑜𝑠 16 𝑛𝑜𝑑𝑜𝑠 𝑚𝑎𝑛𝑑𝑎𝑛 1 𝑚𝑒𝑛𝑠𝑎𝑗𝑒 𝑞𝑢𝑒 𝑎𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 16 1
%𝑀𝑠𝑔 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 = = =
𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝑙𝑜𝑠 16 𝑛𝑜𝑑𝑜𝑠 𝑒𝑛𝑣í𝑎𝑛 4 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑐𝑎𝑑𝑎 𝑢𝑛𝑜 𝑑𝑒 𝑒𝑙𝑙𝑜𝑠 64 4

(c) El ancho de banda generado por cada nodo es de:


𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝐶𝑃𝑈 𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟 = ∙%[ ∙%[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑠𝑔
𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

500 𝑀𝐻𝑧 𝑀𝐵𝑦𝑡𝑒𝑠


= ∙ 0′ 18 ∙ 0′ 18 ∙ 0′ 05 ∙ 144 𝐵𝑦𝑡𝑒𝑠 = 196′23
𝑐𝑖𝑐𝑙𝑜𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜
0′ 5944
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 = 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 + 𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜


𝑐𝑖𝑐𝑙𝑜𝑠 𝑐𝑖𝑐𝑙𝑜𝑠 𝑐𝑖𝑐𝑙𝑜𝑠
= 0′ 4 + 0′ 1944 = 0′ 5944
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = %[ ∙%[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑
𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

𝑐𝑖𝑐𝑙𝑜𝑠
= 0′ 18 ∙ 0′ 18 ∙ 0′ 05 ∙ 120 𝑐.𝐶𝑃𝑈 = 0′ 1944
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑 ≅ 2 ∙ 𝑙 ∙ 𝑡ℎ = 2 ∙ 1 ∙ 60 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈 = 120 𝑐.𝐶𝑃𝑈

4 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑎 1 𝑡𝑟𝑎𝑚𝑜
𝑙 = =1
4 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑔𝑒𝑛𝑒𝑟𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜
500 𝑀𝐻𝑧
𝑡ℎ = 15 𝑐𝑖𝑐𝑙𝑜𝑠𝑅𝑒𝑑 ∙ = 60 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈
125 𝑀𝐻𝑧

𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑒𝑛𝑠𝑎𝑗𝑒 = 𝑃𝑒𝑡𝑖𝑐𝑖ó𝑛 + 𝑅𝑒𝑠𝑝𝑢𝑒𝑠𝑡𝑎 = 𝐶𝑎𝑏𝑒𝑐𝑒𝑟𝑎 + (𝐶𝑎𝑏𝑒𝑐𝑒𝑟𝑎 + 𝐿í𝑛𝑒𝑎 )


= 8 𝐵𝑦𝑡𝑒𝑠 + (8 𝐵𝑦𝑡𝑒𝑠 + 128 𝐵𝑦𝑡𝑒𝑠) = 144 𝐵𝑦𝑡𝑒𝑠

Por tanto:
1 𝑀𝐵𝑦𝑡𝑒𝑠 𝑴𝑩𝒚𝒕𝒆𝒔
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 16 𝑛𝑜𝑑𝑜𝑠 ∙ ∙ 196′23 = 𝟕𝟖𝟒′𝟗𝟑
𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 4 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝒔𝒆𝒈𝒖𝒏𝒅𝒐

124
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Caso: Patrón Todos con Todos.

(a) El número total de nodos = 16 nodos.

(b) El porcentaje medio de mensajes que atraviesan la bisectriz es de:


𝑙𝑜𝑠 16 𝑛𝑜𝑑𝑜𝑠 𝑚𝑎𝑛𝑑𝑎𝑛 8 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑞𝑢𝑒 𝑎𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 8
%𝑀𝑠𝑔 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 = =
𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝑙𝑜𝑠 16 𝑛𝑜𝑑𝑜𝑠 𝑒𝑛𝑣í𝑎𝑛 15 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑐𝑎𝑑𝑎 𝑢𝑛𝑜 𝑑𝑒 𝑒𝑙𝑙𝑜𝑠 15

(c) El ancho de banda generado por cada nodo es de:


𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝐶𝑃𝑈 𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟 = ∙ %[ ∙ %[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑠𝑔
𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

500 𝑀𝐻𝑧 𝑀𝐵𝑦𝑡𝑒𝑠


= ∙ 0′ 18 ∙ 0′ 18 ∙ 0′ 05 ∙ 144 𝐵𝑦𝑡𝑒𝑠 = 143′ 16
𝑐𝑖𝑐𝑙𝑜𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜
0′ 81472
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 = 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 + 𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜


𝑐𝑖𝑐𝑙𝑜𝑠 𝑐𝑖𝑐𝑙𝑜𝑠 𝑐𝑖𝑐𝑙𝑜𝑠
= 0′ 4 + 0′ 41472 = 0′ 81472
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

4m1t
𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = %[ ∙%[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑
𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

𝑐𝑖𝑐𝑙𝑜𝑠
= 0′ 18 ∙ 0′ 18 ∙ 0′ 05 ∙ 256 𝑐.𝐶𝑃𝑈 = 0′ 41472
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛
6m2t

32
𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑 ≅ 2 ∙ 𝑙 ∙ 𝑡ℎ = 2 ∙ ∙ 60 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈 = 256 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈
15

4m3t (4 𝑚 ∙ 1 𝑡) + (6 𝑚 ∙ 2 𝑡) + (4 𝑚 ∙ 3 𝑡) + (1 𝑚 ∙ 4 𝑡) 32
𝑙 = =
15 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑔𝑒𝑛𝑒𝑟𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 15
500 𝑀𝐻𝑧
𝑡ℎ = 15 𝑐𝑖𝑐𝑙𝑜𝑠𝑅𝑒𝑑 ∙ = 60 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈
125 𝑀𝐻𝑧

1m4t

𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑒𝑛𝑠𝑎𝑗𝑒 = 𝑃𝑒𝑡𝑖𝑐𝑖ó𝑛 + 𝑅𝑒𝑠𝑝𝑢𝑒𝑠𝑡𝑎 = 𝐶𝑎𝑏𝑒𝑐𝑒𝑟𝑎 + (𝐶𝑎𝑏𝑒𝑐𝑒𝑟𝑎 + 𝐿í𝑛𝑒𝑎 )


= 8 𝐵𝑦𝑡𝑒𝑠 + (8 𝐵𝑦𝑡𝑒𝑠 + 128 𝐵𝑦𝑡𝑒𝑠) = 144 𝐵𝑦𝑡𝑒𝑠

Por tanto:
8 𝑀𝐵𝑦𝑡𝑒𝑠 𝑴𝑩𝒚𝒕𝒆𝒔
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 16 𝑛𝑜𝑑𝑜𝑠 ∙ ∙ 143′ 16 = 𝟏. 𝟐𝟐𝟏′𝟔𝟖
𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 15 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝒔𝒆𝒈𝒖𝒏𝒅𝒐

125
Conclusión:

Para ambos patrones de comunicación tenemos que el ancho de banda requerido por la aplicación, 784’93 MB/s
y 1.221’68 MB/s, son inferiores al ancho de banda máximo físico soportado por la red, 2.000 MB/s.

Por tanto, el sistema soportará cualquiera de las dos topologías propuestas, aunque irá con más holgura el patrón
adyacente, conexión directa.

126
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio Pregunta 2 del Modelo 2 del Examen 04/06/2013

Pretendemos construir un multiprocesador NUMA aprovechando 12 procesadores “antiguos” (RISC a 800MHz).


También estamos pensando en adquirir un software de optimización para ocultar la latencia, de forma que
evitase el bloqueo de los procesadores en los accesos a la red. La topología sobre la cual queremos montar este
sistema es un anillo bidireccional (numerar los nodos consecutivamente), y el patrón de comunicaciones de la
aplicación es el de que cada nodo par se comunica con todos los nodos impares, y cada nodo impar se comunica
con todos los nodos pares. ¿Cuáles serían los AB mínimos de los enlaces para que la red soportara en la bisección
el ancho de banda generado por el programa, tanto en el caso de usar el software de optimización, como en el
de que no se use?

Datos: %ld/st = 32%; %Acc. Remotos = 18%; Mr = 2’5%; Tamaño Medio Mensaje = 128 Bytes; CPI ideal = 0’5
ciclos/instrucción; El tiempo medio de acceso a la red lo podemos aproximar por: t acc,red = 2 ∗ 𝑙 ∗ 𝑡ℎ donde
th = 25 ciclos CPU.

Solución:
1

12 2

11 3

10 4

9 5

8 6

Calculamos el ancho de banda físico en la bisectriz:


𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑐𝑖ó𝑛 ∙ 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 → 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 2 ∙ 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧
𝐹í𝑠𝑖𝑐𝑜 𝐸𝑛𝑙𝑎𝑐𝑒 𝐹í𝑠𝑖𝑐𝑜 𝐸𝑛𝑙𝑎𝑐𝑒

Calculamos ahora el ancho de banda requerido por el programa para cada patrón de comunicaciones:

𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 𝑁º𝑁𝑜𝑑𝑜𝑠 ∙ %𝑀𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 ∙ 𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟


𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 ⏟ ⏟ 𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ⏟ 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜
(𝑎) (𝑏) (𝑐)

127
Caso optimizado:

(a) El número total de nodos = 12 nodos.

(b) El porcentaje medio de mensajes que atraviesan la bisectriz es de:


𝑙𝑜𝑠 12 𝑛𝑜𝑑𝑜𝑠 𝑚𝑎𝑛𝑑𝑎𝑛 3 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑞𝑢𝑒 𝑎𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 36
%𝑀𝑠𝑔 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 = = = 0′5
𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝑙𝑜𝑠 12 𝑛𝑜𝑑𝑜𝑠 𝑒𝑛𝑣í𝑎𝑛 6 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑐𝑎𝑑𝑎 𝑢𝑛𝑜 𝑑𝑒 𝑒𝑙𝑙𝑜𝑠 72

(c) El ancho de banda generado por cada nodo es de:


𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝐶𝑃𝑈 𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠 ]
𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟 = ∙ %[ ∙ %[ ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑠𝑔
𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

800 𝑀𝐻𝑧 𝑀𝐵𝑦𝑡𝑒𝑠


= ∙ 0′ 32 ∙ 0′ 18 ∙ 0′ 025 ∙ 128 𝐵𝑦𝑡𝑒𝑠 = 295
𝑐𝑖𝑐𝑙𝑜𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜
0′ 5
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛
Por tanto:
𝑀𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 12 𝑛𝑜𝑑𝑜𝑠 ∙ 0′5 ∙ 295 = 1.769
𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

Luego,

𝐴𝐵𝐹í𝑠𝑖𝑐𝑜 ≥ 𝐴𝐵𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 𝑀𝐵𝑦𝑡𝑒𝑠


1.769 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑴𝑩𝒚𝒕𝒆𝒔
𝑀𝐵𝑦𝑡𝑒𝑠 ⟩ → 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ≥ = 𝟖𝟖𝟒′ 𝟓
2 ∙ 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ≥ 1.769 𝐸𝑛𝑙𝑎𝑐𝑒 2 𝒔𝒆𝒈𝒖𝒏𝒅𝒐
𝐸𝑛𝑙𝑎𝑐𝑒 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

128
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Caso no optimizado:

(a) El número total de nodos = 12 nodos.

(b) El porcentaje medio de mensajes que atraviesan la bisectriz es de:


𝑙𝑜𝑠 12 𝑛𝑜𝑑𝑜𝑠 𝑚𝑎𝑛𝑑𝑎𝑛 3 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑞𝑢𝑒 𝑎𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎 𝑙𝑎 𝑏𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 36
%𝑀𝑠𝑔 𝐴𝑡𝑟𝑎𝑣𝑖𝑒𝑠𝑎𝑛 = = = 0′5
𝑙𝑎 𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 𝑙𝑜𝑠 12 𝑛𝑜𝑑𝑜𝑠 𝑒𝑛𝑣í𝑎𝑛 6 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑐𝑎𝑑𝑎 𝑢𝑛𝑜 𝑑𝑒 𝑒𝑙𝑙𝑜𝑠 72

(c) El ancho de banda generado por cada nodo es de:


𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎𝐶𝑃𝑈 𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠 ]
𝐴𝐵𝐺𝑒𝑛𝑒𝑟𝑎𝑑𝑜 𝑝𝑜𝑟 = ∙%[ ∙ %[ ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑎𝑚𝑎ñ𝑜𝑀𝑠𝑔
𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

800 𝑀𝐻𝑧 𝑀𝐵𝑦𝑡𝑒𝑠


= ∙ 0′ 32 ∙ 0′ 18 ∙ 0′ 025 ∙ 128 𝐵𝑦𝑡𝑒𝑠 = 206
𝑐𝑖𝑐𝑙𝑜𝑠 𝑠𝑒𝑔𝑢𝑛𝑑𝑜
0′ 716
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

𝐶𝑃𝐼𝑇𝑂𝑇𝐴𝐿 = 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 + 𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜


𝑐𝑖𝑐𝑙𝑜𝑠 𝑐𝑖𝑐𝑙𝑜𝑠 𝑐𝑖𝑐𝑙𝑜𝑠
= 0′ 5 + 0′ 216 = 0′ 716
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

𝐿𝑜𝑎𝑑𝑠 ] 𝐴𝑐𝑐𝑒𝑠𝑜𝑠
𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = %[ ∙%[ ] ∙ 𝑀𝑖𝑠𝑠𝑟𝑎𝑡𝑒 ∙ 𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑
𝑆𝑡𝑜𝑟𝑒𝑠 𝑟𝑒𝑚𝑜𝑡𝑜𝑠

𝑐𝑖𝑐𝑙𝑜𝑠
= 0′ 32 ∙ 0′ 18 ∙ 0′ 025 ∙ 150 𝑐.𝐶𝑃𝑈 = 0′ 216
𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛

𝑇𝑖𝑒𝑚𝑝𝑜𝐴𝑐𝑐𝑒𝑠𝑜𝑅𝑒𝑑 ≅ 2 ∙ 𝑙 ∙ 𝑡ℎ = 2 ∙ 3 ∙ 25 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈 = 150 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈

(2 𝑚 ∙ 1 𝑡) + (2 𝑚 ∙ 3 𝑡) + (2 𝑚 ∙ 5 𝑡)
𝑙 = =3
6 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑔𝑒𝑛𝑒𝑟𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜

𝑡ℎ = 25 𝑐𝑖𝑐𝑙𝑜𝑠𝐶𝑃𝑈

Por tanto:
𝑀𝐵𝑦𝑡𝑒𝑠 𝑀𝐵𝑦𝑡𝑒𝑠
𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 = 12 𝑛𝑜𝑑𝑜𝑠 ∙ 0′5 ∙ 206 = 1.235′6
𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 𝑠𝑒𝑔𝑢𝑛𝑑𝑜 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

Luego,

𝐴𝐵𝐹í𝑠𝑖𝑐𝑜 ≥ 𝐴𝐵𝑃𝑟𝑜𝑔𝑟𝑎𝑚𝑎 𝑀𝐵𝑦𝑡𝑒𝑠


1.235′ 6 𝑴𝑩𝒚𝒕𝒆𝒔
𝑠𝑒𝑔𝑢𝑛𝑑𝑜
𝑀𝐵𝑦𝑡𝑒𝑠 ⟩ → 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ≥ = 𝟔𝟏𝟕′ 𝟖
2 ∙ 𝐴𝐵𝐵𝑖𝑠𝑒𝑐𝑡𝑟𝑖𝑧 ≥ 1.235′ 6 𝐸𝑛𝑙𝑎𝑐𝑒 2 𝒔𝒆𝒈𝒖𝒏𝒅𝒐
𝐸𝑛𝑙𝑎𝑐𝑒 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

129
130
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

4 Sistemas Distribuidos.

4.1 ¿Qué es un Sistema Distribuido?


Un Sistema Distribuido es una colección de computadores independientes que se muestra al usuario como un
único sistema integrado.

Un sistema distribuido está organizado mediante un middleware. Se trata de una capa que se extiende sobre
múltiples máquinas.

El Middleware determina:

 Cómo se identifican y se asignan los componentes del sistema.


 Cómo interactúan los componentes del sistema.
 La cantidad y granularidad de la comunicación requerida.
 Los protocolos usados para la comunicación.

Ejemplos de Sistemas Distribuidos:

Finance and commerce eCommercee, Amazon, eBay, PayPal, Banca online banking, …
Ingeniería para la información y búsqueda Web, ebooks, Wikipedia; redes sociales:
The information society
Facebook, MySpace, …
Creative industries and Juegos, música y películas online para el hogar, contenido generado por el usuario
entertainment (YouTube, Flickr, …)
Healthcare Informática médica, registro de pacientes online, monitorización de clientes, …
Education e-learning, entornos de aprendizaje virtuales, enseñanza a distancia, …
Transport and logistics Sistemas de búsquedas en ruta GPS, servicios de mapas: Google Maps, Google Earth
Science El Grid como tecnología que posibilita la colaboración entre científicos, …
Environmental
Tecnología de sensores que monitorizan terremotos, inundaciones o tsunamis, …
management

131
4.2 Aspectos en el diseño de un Sistema Distribuido.

Heterogeneidad en:

- Redes.
- Sistemas Operativos.
- Lenguajes de programación.
- Hardware de los dispositivos.

Aun con estas diferencias, se debe asegurar la interacción entre todos los componentes.

Apertura:

- Posibilidad de ampliación.
- Incorporación de nuevos recursos.
- Nuevos servicios compartidos.

Concurrencia.

Posibilidad de acceder a un recurso simultáneamente por distintos componentes.

Seguridad:

Debemos asegurar la información recogida en el sistema mediante la identificación de usuarios y agentes


pertenecientes al sistema, protegiendo así los recursos compartidos y de las transmisiones de la
información.

Componentes de la seguridad de la información:

- Confidencialidad: Protección ante accesos para individuos no autorizados.


- Integridad: Protección contra alteración o corrupción.
- Disponibilidad: Protección contra interferencias al acceso al recurso.

Escalabilidad:

Un sistema es escalable si se mantiene efectivo (no se degrada) cuando hay un incremento significativo
en el número de recursos y en el número de usuarios. Para garantizar la escalabilidad es necesario:

 Controlar el coste de añadir nuevos recursos físicos.


 Controlar la pérdida de rendimiento.
 Evitar el agotamiento de recursos software.
 Evitar puntos que puedan ser cuellos de botella.

Tolerancia a fallos:

Un sistema puede fallar, y seguro que falla. Puede que falle un componente, o una parte del mismo. Pero
el sistema debe seguir funcionando.

La gestión de fallos consiste en:

- Detectar si hay algún fallo.


- Seguir funcionando como si no pasara nada.
- Tolerar los fallos.
- Recuperarse de un fallo.
- Redundancia en los componentes del sistema.

132
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Transparencia:

Oculta ciertos aspectos del sistema a las aplicaciones que actúan en el mismo.

Resulta importante encontrar cierto equilibrio entre nivel alto de transparencia y el rendimiento del sistema.

La transparencia se puede aplicar a distintos aspectos:

- Transparencia de acceso: Permite acceder a recursos locales y remotos con las mismas operaciones.

- Transparencia de ubicación: Permite acceder a un recurso sin conocerse su ubicación real.

- Transparencia de concurrencia: Permite que diferentes usuarios compartan recursos de manera


concurrente.

- Transparencia de replicación: Permite que haya varias instancias en ejecución de un mismo recurso.

- Transparencia de fallos: Permite que el sistema siga funcionando aunque haya algún fallo en algún
componente o recurso.

- Transparencia de movilidad: Permite que los recursos y los usuarios se puedan mover dentro del sistema
sin que afecte a su funcionamiento.

- Transparencia de rendimiento: Permite al sistema ser reconfigurado para mejorar el rendimiento ante
variaciones en la carga de trabajo.

- Transparencia de escalabilidad: Permite al sistema y a las aplicaciones aumentar la escala sin cambios en
la estructura del sistema o en los algoritmos de las aplicaciones que estos utilizan.

4.3 Tipos de arquitecturas.

Vamos a clasificar los Sistemas Distribuidos en función de la ubicación, jerarquía o relación entre los
componentes lógicos. Distinguimos entonces:

 Arquitecturas centralizadas.

 Arquitecturas descentralizadas.

 Publicación-Suscripción.

 Código Móvil.

133
4.3.1 Arquitecturas centralizadas.

Existe una jerarquía definida de manera que unos componentes requieren información o servicios que ofrecen
otros componentes lógicos. Veamos diferentes modelos de arquitecturas centralizadas.

4.3.1.1 Cliente – Servidor.

 Cliente (parte activa)

- Es quien inicia las solicitudes o peticiones (maestro).


- Espera y recibe las respuestas del servidor.
- Puede conectarse a varios servidores simultáneamente.
- Está diseñado para soportar la interacción con los usuarios finales.
- Generalmente no comparte sus propios recursos con otros clientes.
- No suelen tener restricciones especiales respecto al rendimiento, fiabilidad y escalabilidad, ya
que no suele requerir equipos de altas prestaciones y, ante un fallo en un cliente, el resto del
sistema no queda afectado.
- Debe dar soporte a restricciones relativas a ergonomía (facilidad de uso) y seguridad (evitar
comprometer los demás componentes).

 Servidor (parte pasiva)

- Es quien espera las solicitudes o peticiones de los clientes (esclavo).


- Procesa esas peticiones y les envía una respuesta.
- Diseño orientado a maximizar la eficiencia. Raramente interactúa con el usuario final.
- Los servidores pueden ser a su vez, clientes de otros servidores: Correo web, buscadores, etc.
- Tienen restricciones concretas respecto a rendimiento, fiabilidad, escalabilidad y seguridad:
- Capacidad suficiente para atender múltiples clientes.
- Los fallos en el servidor son críticos e invalidan el sistema.
- Aumentar el número de clientes (peticiones) si así se requiere.
- Evitar comprometer la seguridad de los recursos o datos gestionados y de los clientes.

134
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Casos particulares Cliente-Servidor:

Servicios proporcionados por múltiples servidores:

Muchos servicios de comercio Web de hoy en día están implementados en diferentes servidores. Los
cuales mantienen bases de datos replicadas o distribuidas. Su principal motivación es el desempeño
(ejemplo: cnn.com, servidores para descarga, etc.) y la confiabilidad.

Servidores Proxy:

Suministran replicación/distribución de forma transparente. Se basan en técnicas de caching, esto es,


mantienen cachés, como almacenes de recursos solicitados recientemente, por lo que es muy utilizado
por los motores de búsqueda.

Código móvil:

Código enviado a un proceso cliente para que realice una tarea específica.
Ejemplos: Applets de Java y los Mensajes Activos conteniendo código de protocolo de comunicación.

Paso 1) El cliente solicita la descarga del Applet Paso 2) El cliente interactúa con el Applet.

Agentes Móviles:

Objeto que puede moverse autónomamente a lo largo de la red desde un host hasta otro, con su código,
datos y estado de ejecución y realizar ciertas tareas en nombre de un usuario (Mercados virtuales,
programas gusano). Sus principales ventajas son la flexibilidad y el ahorro en costo de comunicación.

135
4.3.1.2 Arquitectura multicapa o multiestrato (n-tier).

Se trata de una generalización del modelo Cliente-Servidor.

Los componentes pueden emitir peticiones y responder a estos a cerca de los recursos concretos que gestionan.
Esto es, se convierten en clientes y servidores a la vez.

Características principales:

 La funcionalidad ofrecida está distribuida entre distintas plataformas u ordenadores.


 La interfaz reside en el ordenador del usuario.
 Los servicios funcionales pueden estar en uno o más ordenadores.
 Los datos y sistemas propietarios están en plataformas adicionales.
 Las arquitecturas multicapas más habituales son las de dos y tres capas.
 Puede utilizar clientes ligeros, pesados o híbridos.

 Arquitectura de 2 capas (two tier)

Está formada por tres componentes distribuidos en dos niveles (nivel cliente y nivel servidor):

- Interfaz de usuario del sistema.


- Capacidad de procesamiento.
- Gestión de datos (servicio de datos y ficheros).

Un único servidor atiende a múltiples clientes.

Problemas de 2-tier:

- Escasa escalabilidad en servidores con lógica de negocio compleja o con grandes bases de datos,
lo que dificulta, por ejemplo, la replicación.
- Rigidez, modificaciones en la lógica de aplicación, ya que supondrían grandes cambios a realizar
en la totalidad de los clientes.
- Difícil evolución del servidor.
- Alto acoplamiento/dependencia del cliente respecto del servidor.

136
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

 Arquitectura de 3 capas (three tier)

Extensión del modelo tradicional que pretende aumentar el desacoplamiento entre servidor y clientes,
que introduce una capa intermedia separando el servidor en 2 componentes:

- El cliente se dedica casi exclusivamente a la interfaz de usuario.


- El servidor de datos comparte con servidor del nivel intermedio la lógica de la aplicación. Su
reparto preciso depende del modelo concreto seguido.

 Arquitectura de n-capas (n-tier)

Se trata de una generalización del modelo 3-tier añadiendo nuevas capas.

La lógica de aplicación se reparte en diferentes capas/niveles ubicadas entre el cliente y los datos, donde
las capas intermedias proporcionan servicios entre sí. De esta forma, cada nivel se comunica sólo con los
niveles contiguos a través de interfaces bien definidas; esto es, la capa k ofrece servicios a capa k - 1 y
demanda servicios de capa k + 1.

Es una estructura típica de los sistemas basados en componentes distribuidos (objetos distribuidos).

137
4.3.2 Arquitecturas descentralizadas.

Se trata de una distribución horizontal, donde un cliente o un servidor puede estar físicamente dividido en partes
lógicamente equivalentes, pero cada una de dichas partes opera con su propio conjunto integral de datos,
balanceando así la carga del sistema.

Los diferentes componentes del sistema actúan como clientes y como servidores al mismo tiempo.

Las arquitecturas descentralizadas forman los sistemas llamados Peer-to-Peer (P2P).

4.3.2.1 Peer-to-Peer (P2P).

Ponen parte de sus recursos (procesamiento, disco, ancho de banda, etc.) a disposición directa del resto de
componentes sin la necesidad de una instancia de coordinación central.

Forman redes superpuestas (overlay networks): No estructuradas / Estructuradas / Híbridas.

(a) Sistema Peer-to-Peer (P2P) (b) Sistema centralizado con servidor

Napster:

Napster introduce grandes cambios:

- Autonomía de los usuarios. Los ficheros disponibles son los que los usuarios deciden aportar.
- Conectividad puntual (ad-hoc). La disponibilidad de un fichero depende si lo tiene algún usuario
conectado.
- Tolerancia a fallos. Muchos usuarios ofrecen el mismo fichero.
- Coste del sistema. Los recursos para almacenar los ficheros son proporcionados por los usuarios.
- Auto organización. El sistema evoluciona y se adapta a medida que los usuarios se conectan o se
desconectan.
- Escalabilidad. El sistema soporta muchos usuarios sin que se degrade.
- Mejora del rendimiento. Al haber muchas réplicas de un mismo fichero, el proceso de
carga/descarga está repartida.

138
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

4.3.2.2 No estructuradas.

Se trata de sistemas de iguales (peer) que se conectan sin conocer la topología de la red, que se sobreponen
fácilmente ante las conexiones y desconexiones de los nodos.

Las consultas se envían usando técnicas para epidémicas, esto es, los iguales envían las peticiones a los nodos
vecinos, que contienen la información devuelven los resultados. Estas técnicas, basadas en inundación, son útiles
para localizar objetos altamente replicados.

Estas arquitecturas descentralizadas no estructuradas plantean ciertos problemas:

- No tienen buen comportamiento ante búsquedas de objetos que están poco replicados.
- Cuando tienen que gestionar un ritmo elevado de consultas, o hay crecimientos repentinos del sistema,
se sobrecargan y tienen problemas de escalabilidad.

Técnicas de búsquedas en arquitecturas descentralizadas no estructuradas:

Inundación (flooding). La búsqueda se realiza usando todos los nodos vecinos.

El protocolo de búsqueda es muy simple: un nodo envía una petición a sus nodos vecinos, y estos reenvían la
petición a sus nodos vecinos respectivamente y, cuando encuentran el objeto buscado, responde al nodo emisor.

- Ventaja: Se adapta perfectamente a entradas y salidas de nodos en el sistema.

- Desventaja: Poca escalabilidad, ya que los costes de la búsqueda crecen exponencialmente a medida
que el número de nodos aumenta. A pesar de esta desventaja, sistemas como Gnutella han demostrado
su viabilidad en redes de tamaño medio.

Caminos aleatorios (random walks). La búsqueda usando un número de nodos vecinos concretos y aleatorios.
Este sistema es más escalable que el método de inundación.

Caminos aleatorios adaptativos. Mejora de los caminos aleatorios.

Controla el radio de búsqueda mediante TTL (Time To Live) del mensaje de búsqueda, iniciando la búsqueda con
un TTL de 1, si no encontramos el objeto entonces iniciamos una nueva búsqueda con TTL de 2, y así
sucesivamente hasta encontrar el objeto o cesar el proceso de búsqueda.

Otra mejora es poder seleccionar el siguiente paso en función de valores de ciertos parámetros como puede ser
la popularidad estimada del siguiente nodo, seleccionando así el nodo con más posibilidades de tener el objeto
que buscamos.
139
Mecanismos para el mantenimiento de la información.

Se requiere de mecanismos para mantener la consistencia de la información distribuida en sistemas con


arquitectura descentralizada no estructurados, tales como el estado en el que se encuentra del sistema,
información sobre la topología de la red, etc.

Vista parcial. Cada nodo conoce la lista de sus nodos vecinos. Así, para mantenerla se utilizan mecanismos
epidémicos (gossip). Cuando dos nodos se conectan, comparten su información y la que han recibido de otros
nodos, propagándose la información y llegando a “infectar” el sistema completo.

Condiciones de los mecanismos epidémicos:

- La interacción de las parejas de manera periódica.


- La información compartida es escasa.
- Cambios de estado de los nodos después de la comunicación.
- La frecuencia de las iteraciones es pequeña.
- Existe cierta aleatoriedad en la selección de los compontes con los que se interactúa.

La dirección, así como la proactividad de los componentes, diferencian dos modos de operación:

- Modo de envío inducido (pull mode): Los nodos esperan pasivos a recibir información de otros y, sólo un
nodo actualiza su información cuando otro se pone en contacto con él.

- Modo de envío automático (push mode): Los nodos, de manera periódica, se comunican con algún otro
nodo con el fin de actualizar su información.

Ejemplos de sistemas no estructurados:

Gnutella Freenet

Para localizar un objeto, un nodo pregunta a sus Red descentralizada para el intercambio de
vecinos y estos inundan a sus nodos vecinos hasta información.
un determinado radio. Cuando la petición llega a Su objetivo es mantener el anonimato de sus
un nodo que ofrece el objeto, este contesta usuarios, tanto de los que aportan información como
directamente al nodo que inició la búsqueda. La de los que acceden a ella. Cada nodo guarda
descarga se realiza directamente de los nodos que información y aporta información, de forma que, una
contestan a la búsqueda. vez añadido un fichero a la red, se hace casi imposible
saber de dónde proviene o quién lo ha originado. No
es posible eliminar información de la red, sino que
esta desaparece a medida de que deja de ser
accedida.

140
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

4.3.2.3 Estructuradas.

Topología fuertemente controlada donde el contenido va a determinados nodos para optimizar las consultas.

Para llevarlo a cabo se usan tablas hash distribuidas de esta forma:

- La ubicación de los objetos se hace de forma determinista.

- Los identificadores de los objetos se asignan de manera consistente a los iguales, dentro de un espacio
con muchos identificadores.

- Se asigna un identificador a cada objeto de datos, esto es, una clave.

- El protocolo mapea las claves a un único nodo entre los que están conectados.

- Las redes soportan el almacenamiento y la recuperación de pares <clave, valor>.

DHT (Distributed Hash Tables):

Son la evolución de los modelos peer-to-peer (Napster y Gnutella), que permite la localización eficiente de datos
utilizando un índice descentralizado y uniformemente repartido entre los nodos del sistema. Cada nodo es
análogo a una celda de una tabla hash que permite almacenamiento y recuperación de información de manera
eficiente en un entorno distribuido.

141
4.3.2.4 Sistemas Híbridos.

Super-peers (Super-igual):

Un super-igual es un nodo que actúa como índice o gestor de la información del sistema. Suelen ser nodos que
se mantienen conectados y sufren poco dinamismo durante la vida del sistema. Se organizan en una red tipo
peer-to-peer formando una estructura de dos o más niveles. El problema es la elección de los nodos super-
iguales.

KaZaA

En el proceso de búsqueda, el peer hace una pregunta al super-peer al que está conectado y
este hace un broadcast a los otros super-peers.

eDonkey

Sistema organizado en dos niveles: Clientes y Servidores.

Los servidores actúan como concentradores para los clientes y les permite localizar los ficheros.
Controla la bajada concurrente de ficheros desde varias ubicaciones, el uso de funciones
resume para detección de ficheros corruptos, la compartición parcial de ficheros mientras se
bajan y además cuentan con métodos expresivos para realizar búsquedas de ficheros.

Para que un nodo pueda conectarse al sistema, necesita conocer la ubicación de algún peer
que actúe como servidor. Al conectarse, el cliente manda al servidor un listado de los ficheros
que comparte.

Skype

Funciona usando una organización de super-peers similar a la de KaZaA (Skype fue fundada por
los creadores de KaZaA).

142
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Trackers:

Se trata de servidores dedicados a mantener información sobre un conjunto de peers que comparten o
almacenan un determinado contenido.

BitTorrent

Sistema para distribuir grandes volúmenes de datos.

El nodo que origina la información no tiene que soportar todo el coste de los recursos para servir
dicha información, sino que los servidores almacenan un fichero que contiene la información
sobre un fichero compartido (longitud, nombre, información de resumen y la URL del tracker).

El tracker conoce todos los peers que tienen el fichero (tanto total como parcialmente) y hace que
los peers se conecten para subir o bajar dichos ficheros.

Cuando un nodo quiere bajar un fichero:

- Envía un mensaje al tracker.

- El tracker le devuelve una lista aleatoria de nodos que están descargando el mismo fichero
en ese momento.

- BitTorrent parte los ficheros en trozos de 256 KB para poder saber qué tiene cada uno de
los peers.

- Cada peer que está bajando el fichero, anuncia al resto de peers qué trozos tiene.

- El protocolo cuenta con mecanismos para penalizar a los usuarios que no comparten.

143
4.3.3 Publicación-Suscripción.

Implementable sobre arquitecturas centralizadas, descentralizadas o híbridas.

Paradigma:

1. El productor de la información publica la disponibilidad de cierto tipo de información en el mediador.


2. El consumidor pregunta sobre datos específicos y se suscribe al productor mediante el mediador.
3. El productor, periódicamente, publica datos actualizados en el mediador.
4. El mediador entrega datos actualizados al consumidor.
5. Cuando el consumidor quiere pedir datos al productor o enviar datos volátiles o grandes volúmenes de
datos se utiliza un enlace interactivo que une directamente el productor con el consumidor.

Componentes de la arquitectura P/S:

Productor de información
- Aplicación que contiene la información que hay que difundir.
- La información se publica sin saber quién la recibirá.
- La información se envía a través de canales.

Consumidor de información
- Aplicación interesada en recibir información.
- El consumidor se suscribe a los canales que diseminan la información que le interesa.

Mediador
- Aplicación que esté entre el productor y el consumidor.
- Recibe información actualizada de los productores y peticiones de suscripción de los consumidores.
- Encamina la información publicada a los destinatarios suscritos al canal.

Canal
Son conectores lógicos entre los productores y los consumidores, y determina:

- Tipo de la información que se comparte.


- Formato de los datos.
- Personalización del canal por parte del usuario: selección de contenidos, modos de operación, etc.
- Persistencia del contenido.
- Estrategia de las actualizaciones.
- Modo de entrega: Inmediato, diferido...
- Modo de operación y de pago.

Comunicación entre productor y consumidor.


144
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ventajas del paradigma P/S:

Localización:

- Para los usuarios es un problema saber dónde está la información que le interesa.
- En los sistemas P/S el usuario se suscribe a unos canales, y ahora es el proveedor de información
quien que se encarga de hacerla llegar a los usuarios interesados.

Focalización:

- El usuario dice explícitamente cuáles son sus preferencias.


- Es sencillo proporcionar la información centrada en los intereses del usuario.

Personalización:

- El usuario puede especificar ciertos requerimientos, antes de que los datos y sus propiedades se
entreguen, por ejemplo, el formato de los datos, la prioridad, palabras clave, etc.

Actualidad:

- Los datos se pueden difundir a medida que estos se encuentren disponibles.


- El proveedor de información puede invalidar los datos obsoletos.

Adaptación (tailoring):

- El proveedor puede decidir qué información ve el receptor y cuál no.

Reducción del tráfico:

- La información fluye sólo hacia los consumidores que desean obtenerla, reduciendo así el tráfico.

Casos de uso del paradigma P/S:

- Grupos de noticias y listas de distribución de correo.


- Bolsa y noticias.
- Sistemas de información del estado del tráfico.
- Distribución de software.
- Servicios de alerta, monitorizaciones, vigilancia y de control.

145
4.3.4 Código móvil.

Objetivos:

 Usar la movilidad para cambiar dinámicamente la distancia entre el procesamiento, la fuente y el destino
de los datos. Para mejorar la proximidad la calidad de las interacciones.

 Reducir el coste de las interacciones.

 Mejorar la eficiencia y percepción del usuario sobre el rendimiento.

Máquinas virtuales.

Lenguajes de Scripting.

Lenguajes de programación generalmente interpretado. Algunos tipos:

o Programación web:
- En el lado del servidor: PHP, ASP (Active Server Pages), JSP (JavaServerPages), ColdFusion,
IPTSCRAE, Lasso, MIVA Script, SMX, XSLT, …
- En el lado del cliente: JavaScript, JScript, VBScript, Tcl, …

o Perl.

o PostScript.

146
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Paradigma de código móvil:

Frente al paradigma clásico cliente/servidor, surge la idea de movilidad de código. Veamos algunas formas de
llevar a cabo una tarea:

1. Hazla tú mismo.

El propio sistema cuenta con toda la información y capacidad de procesado.

2. Pedirle a otro sistema que la haga: Cliente/Servidor.

3. Decirle a otro sistema la secuencia de pasos a ejecutar: Evaluación remota.

El cliente tiene el conocimiento para realizar una tarea pero no tiene los recursos necesarios (potencia de
cálculo, datos, etc.) así que los recursos necesarios se encuentran en un ordenador remoto.

Se presupone que el código proporcionado se ejecutará en un entorno protegido, esto es, un cliente no
debe interferir en otros clientes por lo que existe una cierta confianza del servidor en los clientes.

El ordenador que ejecuta el código remoto ofrece un servicio programable, un lenguaje de programación
completo. Ejemplos de estos lenguajes son:

- Rshde Unix: Permite ejecutar archivos de comandos (scripts) en un ordenador remoto.

- PostScript: Permite la interacción entre un procesador de textos y una impresora (recurso), la cual
tiene un intérprete del lenguaje PostScript que ejecuta el código (fichero PostScript).

147
4. Pregúntale a otro sistema cómo se realiza: Código bajo demanda.

El cliente tiene acceso a un conjunto de recursos, pero el cliente no tiene el conocimiento necesario para
procesarlos, por lo que envía una petición al servidor remoto y este le envía el código necesario para llevarlo
a cabo. El cliente debe confiar en el servidor que le suministra el código y ejecuta el código localmente.
Ejemplo más conocido: Applet

Los applets permiten añadir funcionalidades a un cliente sin tener que modificarlo.

5. Agente móvil.

Una unidad de computación se mueve a un ordenador remoto, llevándose su estado, parte de código que
necesita y los datos necesarios para llevar a cabo la tarea.

Aporta más dinamismo al sistema, ya que se puede decidir cuándo mover el código de un ordenador a otro
para mejorar el rendimiento y acercar así el código hacia los datos que tiene que procesar.

Son muy útiles cuando el volumen de datos a procesar es muy elevado y se encuentran muy distribuidos.

148
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5 Aplicaciones de sistemas distribuidos.

5.1 Sistemas computacionales distribuidos: Clústeres.

Los sistemas computacionales distribuidos están formados


principalmente por:

- Clústeres.
- Grid.
- Desktop Grid.
- Cloud Computing.

El término clúster (del inglés cluster, "grupo" o "raíz") se aplica a los conjuntos de computadoras unidos entre sí
normalmente por una red de alta velocidad y que se comportan como si fuesen una única computadora. La
tecnología de clústeres ha evolucionado en apoyo de actividades que van desde aplicaciones de supercómputo
y software para aplicaciones críticas, servidores web y comercio electrónico, hasta bases de datos de alto
rendimiento, entre otros usos.

El cómputo con clústeres surge como resultado de la convergencia de varias tendencias actuales que incluyen la
disponibilidad de microprocesadores económicos de alto rendimiento y redes de alta velocidad, el desarrollo de
herramientas de software para cómputo distribuido de alto rendimiento, así como la creciente necesidad de
potencia computacional para aplicaciones que la requieran.

Simplemente, un clúster es un grupo de múltiples ordenadores unidos mediante una red de alta velocidad, de
tal forma que el conjunto es visto como un único ordenador, más potente que los comunes de escritorio. Los
clústeres son usualmente empleados para mejorar el rendimiento y/o la disponibilidad por encima de la que es
provista por un solo computador típicamente siendo más económico que computadores individuales de rapidez
y disponibilidad comparables.

Los clústeres determinan una arquitectura distribuida formada por un conjunto de computadores
independientes interconectados que funciona como un único sistema (SSI, Single System Image) que intenta dar
la visión de cara al usuario de un sistema único. Todo el clúster se muestra como un monoprocesador virtual.

149
5.2 Clúster Beowulf.

El término Beowulf hace referencia a un sistema de cómputo paralelo basado


en clústeres de ordenadores personales conectados a través de redes
informáticas standard, sin el uso de equipos desarrollados específicamente
para la computación paralela.

Fue desarrollado por primera vez en 1994 por Donald Becker y Thomas
Sterling para la NASA. Agruparon 16 procesadores Intel DX4 de 100 MHz, los
interconectaron con tecnología vía channel bonded Ethernet a 10 Mbps, en Un cluster Beowulf casero
equipos viejos con Linux instalado como sistema operativo, pero más tarde
se continuó el desarrollo una comunidad internacional interesados en la computación científica. En 2001 aparece
Locus Supercluster, con 1.416 procesadores Pentium III 1 GHz (708 nodos, cada nodo 2 procesadores) 364 GB de
RAM y 22.240 GB de HDD, SO Linux y tecnología de interconexión Fast Ethernet a 1.416 Gflops (su principal área
de aplicación es el descubrimiento de nuevas sustancias para uso farmacéutico).

Un clúster Beowulf es por lo general un grupo de computadoras compatibles con IBM, sin monitor, ratón ni
tarjeta de video, que utiliza software de código abierto. Para que un grupo de computadoras pueda ser
considerado un Beowulf deben cumplir ciertos requisitos como, contar con un nodo como servidor (front-end),
disponer de un sistema de ficheros compartido y el uso de MPI como herramienta de programación paralela.

Visión lógica de un clúster Beowulf:

En otras palabras, un Beowulf es un conjunto de nodos minimalistas (cada uno de ellos tiene lo mínimo para
poder funcionar), ligados por un medio de comunicaciones barato, en el que la topología de red se ha diseñado
para resolver un tipo de problema específico. En dicho conjunto se ejecutan aplicaciones paralelas.

Cada nodo habitualmente consta de placa madre, CPU, memoria y dispositivo de comunicaciones, y donde uno
de los nodos es el nodo servidor. Se trata de un ordenador completo. Todos los nodos emplean Linux como SO.
La programación es fuertemente dependiente de la arquitectura; se pueden usar sockets o librerías de paso de
mensajes (PVM o MPI). Debido a que los Beowulf emplean memoria distribuida, la comunicación se hace
mediante paso de mensajes. Si un programa está paralelizado usando PVM, MPI o sockets, la adaptación al
Beowulf es trivial. Si no lo está, la paralelización tiene un cierto grado de dificultad.

La mayor parte del software para Beowulf es de dominio público (sistemas operativos, compiladores, librerías,
programas específicos,etc.).

150
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.3 Clasificación de los clústeres.

Los clústeres podemos clasificarlos en:

 Clústeres de altas prestaciones (HPC, High Performance Clusters): Aplicaciones paralelas.


 Clústeres de alta eficiencia (HTC, High Throughput Clusters): Gran número de tareas independientes.
 Clústeres de alta disponibilidad (HA, High Availability Clusters): Aplicaciones críticas.
 Clústeres de balanceo de carga: Servidores web, de correo, servidores en general.
 Clústeres híbridos: HPC + HA.
 Según la dedicación: Dedicados, no dedicados.
 Según la configuración: Homogéneos y heterogéneos.

Sistemas de gestión de recursos:

- LSF: http://www.platform.com/
- SGE: http://www.sun.com/grid/
- NQE: http://www.cray.com/
- LL: http://www.ibm.com/systems/clusters/software/loadleveler/
- PBS: http://www.pbsgridworks.com/

5.4 Clústeres de Workstations (COW).

En la vida real hay muchos problemas en los que la potencia computacional se queda corta, tales como la
meteorología, la física de la materia condensada, el estudio de las proteínas, el de la generación de imágenes
por computador con modelos realísticos, el secuenciamiento del genoma humano, etc. Así pues, es necesario
recurrir a los supercomputadores.

Sin embargo, no todos los laboratorios se pueden permitir la compra de un supercomputador prefabricado que
cubra sus necesidades, para lo que requieren de una gran inversión, y cuentan con escasos presupuestos.
Entonces, ¿Cómo consiguen la potencia necesaria aun teniendo el problema del presupuesto? Hay dos
alternativas:

 Conseguir máquinas viejas o comprar máquinas nuevas de baja potencia y montar Linux en todas ellas
e instalar encima una MOSIX (Cluster Management System).

 Con el mismo coste que la alternativa anterior se puede construir un supercomputador de clase Beowulf,
que ofrece un mayor rendimiento.

De las dos maneras que acabamos de describir tenemos un supercomputador de bajo coste.

¿Qué es un COW?

Un Clúster Of Workstations (COW) es un conjunto de nodos de potencia computacional baja o media (es decir
hardware con la mejor relación FLOP/$ posible) que comunicamos entre sí mediante una red para resolver un
determinado problema.

Los nodos del COW son normalmente estaciones de trabajo funcionales (CPU, RAM, HDD, tarjeta gráfica,
monitor, teclado) y, excepcionalmente, son nodos minimalistas.

151
¿Por qué surgen los COW?

Para diseñar un programa paralelo basado en Beowulf, el conocimiento de la topología de red y del número de
procesadores son críticos.

Debemos tener en cuenta que un Beowulf, en principio, no hace reparto automático de carga, por lo que un
programa, teniendo en cuenta la topología del Beowulf, es más rápido que el equivalente en un COW, por la
ausencia de colisiones. Programar un código capaz de adaptarse al Beowulf (topología de red y número de
procesadores) complica su desarrollo del código. Si no tenemos el código fuente de una aplicación y deseamos
adaptarla a un clúster paralelo, emplear una arquitectura Beowulf lleva a un código poco eficiente.

Los COW se ejecutan sobre alguna versión de UNIX, como Linux y tienen instalado MOSIX. MOSIX es un software
diseñado específicamente para incorporar al kernel la capacidad de computación en clúster. En los COW, el kernel
de los distintos nodos ajusta la carga migrando algunos procesos, en caso de que se encuentre que un nodo está
con carga excesiva o con poca memoria. Este ajuste automático de carga entre nodos, con migraciones de
procesos entre nodos, constituye la columna vertebral de los COW y su gran diferencia con los Beowulf.

Los algoritmos de MOSIX son distribuidos, esto es, cada nodo es maestro para los procesos que se crearon
localmente y servidor para los procesos remotos que migraron desde otros nodos.

Cualquier máquina perteneciente al COW, en la que entremos, se comportará como una única máquina SMP
con tantos procesadores como tenga todo el clúster. Esa visión de máquina SMP la comparten tanto el usuario
que lanza la aplicación como el programador que la desarrolla. Nos podemos despreocupar completamente con
la paralelización del código y el reparto de procesadores, porque de eso se encarga MOSIX.

Los COW dan toda su potencia mediante el modelo de programación fork and forget. Si deseamos emplear el
paralelismo de un COW con toda su potencia, nos basta con programar haciendo llamadas a fork.

Beowulf vs COW

 Cada nodo de un Beowulf se dedica exclusivamente a procesos del supercomputador y no sirven para
otro propósito. Cada nodo de un COW puede servir para diversos propósitos ya que puede funcionar de
forma independiente, es decir, sin necesidad de participar del sistema distribuido.

 Los nodos Beowulf son más baratos que los nodos COW, ya que no necesitan monitor, teclado, ratón.
Por tanto, con igual presupuesto, podemos tener un mayor número de nodos Beowulf y más rápidos.

 En un Beowulf no nos hemos de preocupar de la seguridad interna:

- No hay usuarios entrando en los distintos nodos de un Beowulf.

- La red que une los nodos de un Beowulf es privada y está dedicada exclusivamente al Beowulf
(tenemos más ancho de banda en la red, para nosotros).

- Los nodos de un Beowulf no son accesibles directamente desde fuera, pero en los de un COW sí
que puede darse el caso.

 A la hora de programar nos da igual la topología de la red porque MOSIX nos abstrae de ella. En Beowulf
la topología sí es importante a la hora de programar una aplicación.

 La programación en COW es fácil usando el modelo fork and forget.

152
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

 En un COW el usuario no sabe nada acerca de la máquina, ni cómo configurarla, ni cómo montarla.
Habitualmente, el usuario y el administrador son dos personas distintas. En un Beowulf el equipo de
desarrollo del programa (o el usuario en caso de ser sólo uno) tienen que saber desde diseñar una
topología de red y hacer aplicaciones paralelas hasta cómo montar los ordenadores.

 Un COW tiene un número determinado de usuarios heterogéneos, con una cuenta por usuario, un
administrador y un conjunto indeterminado de problemas de cada usuario para resolver. Un Beowulf se
desarrolla para resolver un problema; habitualmente sólo resuelve un mismo tipo de problemas al
mismo tiempo. Tiene un usuario que opera con la máquina y la administra (un gurú en redes y en Linux)
así como un grupo de expertos que le da al gurú los problemas para que los resuelva.

 Un COW no necesita que se recompilen los programas, mientras que Beowulf sí.

 Un COW permite añadir (mejora automática del rendimiento de aplicaciones paralelas que ya estén en
ejecución, sin necesitar ser reiniciadas) y eliminar nodos en caliente.

 Los COW dan mejores respuestas que los Beowulf cuando cae un nodo.

 En un COW todos los nodos han de ser de la misma arquitectura, tener CPUs con juegos de instrucciones
compatibles y una misma distribución de UNIX.

 Hoy en día un COW sólo corre en plataforma Intel. Necesita como mínimo un 386 con coprocesador (no
funciona con 486SX).

 Aparte de un espacio de procesos común, un mejor aprovechamiento de la red o mejoras en la


paginación, tal como ocurría en Beowulf, en los COW los procesos migran de un nodo a otro según la
carga de las máquinas y la memoria libre (de forma transparente), empleando para ello un algoritmo de
planificación adaptativo y distribuido. Dicho mecanismo de migración de procesos nos añade una
sobrecarga al sistema.

 Es mejor un Beowulf si disponemos del código fuente de la aplicación para adaptar el modelo de
paralelismo de la aplicación a la topología de la red y al número de procesadores. Hacer esto es
interesante y está justificado por los beneficios que obtendremos.

 Los COW son mejores que los Beowulf en los siguientes casos:

- Variación frecuente de las aplicaciones.

- Varias aplicaciones distintas ejecutándose en el mismo clúster de forma simultánea.

- El incremento de carga causado por el mecanismo de migración justifica la ventaja de no tener


los nodos desocupados.

- Tenemos aplicaciones, previamente paralelizadas, que se ejecutaran en un COW de un modo


cuasi óptimo. El mejor escenario para un COW es cuando tenemos la aplicación paralelizada
pero no disponemos de su código fuente. En este caso, un COW es la mejor forma de correr
dicha aplicación lo más rápidamente posible.

 Ambas son tecnologías suelen correr bajo Linux y proporcionan supercomputación a bajo coste.

 En aplicaciones cuyo tiempo de ejecución dependa fundamentalmente de la velocidad de acceso a disco,


de la velocidad de la red o aquellas que usen modelos de memoria compartida, tienen un rendimiento
pobre tanto en Beowulf como en COW.

153
Concluyendo:

Un COW es una solución rápida de configurar, fácil de programar, de una potencia computacional buena
y no exige demasiados conocimientos a los usuarios que operan con las máquinas.

Un Beowulf exige gran cantidad de conocimientos y dedicación para la puesta en marcha, desarrollo y
ejecución; a cambio su rendimiento es realmente impresionante, en comparación al bajo costo
necesario. El usuario-operador se encarga de tener todo optimizado para el problema a resolver.

Ejemplo de PCs completos conectados por una red, aprovechando los tiempos no usados:

5.5 Estaciones de trabajo inactivas.

En entornos típicos con estaciones de trabajo se desperdicia cerca del 80% de ciclos totales de CPU. Por lo que
se promueve el uso de estas estaciones de trabajo ejecutando procesos de forma totalmente transparente en
máquinas remotas que se encuentran inactivas.

Los usuarios de las estaciones de trabajo inactivas no deberían observar degradación alguna del rendimiento
como consecuencia de la ejecución de los procesos remotos.

5.6 Objetivos del diseño de Clústeres.

 Escalabilidad, que puede verse limitada por:


- Tecnología del procesador.
- Topología del Clúster.
- Modelo de empaquetamiento.
- Consumo de potencia.
- Esquema de refrigeración utilizado.
- Cuello de botella en la E/S a disco.
 Single System Image.
 Disponibilidad.
 Gestión de trabajos.
 Tolerancia a fallos.
 Comunicación.

154
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.7 Componentes del IBM Blue Geen/L.

Blue Gene es un supercomputador desarrollado por IBM que se convirtió


en 2005 en el ordenador más rápido del mundo. Está instalado en el
laboratorio estadounidense Lawrence Livermore. Esta máquina se dedica
principalmente al almacenamiento y transmisión de datos entre diversos
sistemas informáticos. Gracias a esta computadora, EEUU volvió a
encabezar la lista de las máquinas más potentes del mundo, arrebatándole
el título a Japón, que lo tenía desde 2002 con el Earth Simulator.

Esta supercomputadora de IBM consiguió resolver las ecuaciones del Test de Linpack a una velocidad de 36’01
teraflops (36’01 billones de operaciones en punto flotante por segundo). Superó por poco a la máquina japonesa
que la ostentaba en 35’86 teraflops. Sin embargo hay que tener en cuenta que los FLOPS como métrica de
rendimiento únicamente evalúan los cálculos de operaciones matemáticas con números decimales sin embargo
el rendimiento de un sistema no puede medirse por este único parámetro ya que es claro que hay otras
operaciones vitales tales como las operaciones de transferencia de información entre distintos procesadores y/o
unidades lógicas o funcionales dentro del sistema.

El Blue Gene/L ocupa unos 320 metros cuadrados (100 veces menos que la nipona) y consume tan solo 216
kilovatios, frente a los 6.000 que gasta su rival.

155
5.8 Grid.

Escenario típico, la Agencia Espacial Europea (ESA, European Space Agency). Los datos científicos recibidos de
los satélites son importantes para los científicos. Por ejemplo, la Estación de Seguimiento de Satélites de
Villafranca del Castillo (VILSPA) mantiene un archivo con todos los datos científicos obtenidos de las misiones
completadas y en marcha:

- El observatorio XMM-Newton.
- El observatorio espacial de infrarrojos.
- El explorador internacional de ultravioletas.

Esta información está disponible y se comparte con la comunidad científica para fines de investigación.

5.8.1 Grid computing.

La computación Grid es una tecnología innovadora que permite utilizar de forma coordinada todo tipo de
recursos (entre ellos cómputo, almacenamiento y aplicaciones específicas) que no están sujetos a un control
centralizado. En este sentido es una nueva forma de computación distribuida, en la que los recursos pueden ser
heterogéneos (diferentes arquitecturas, supercomputadores, clústeres...) y se encuentran conectados mediante
redes de área extensa (por ejemplo Internet). Desarrollado en ámbitos científicos a principios de 1990, su
entrada al mercado comercial sigue la idea de la llamada Utility computing supone una importante revolución.

El término Grid se refiere a una infraestructura que permite la integración y el uso colectivo de ordenadores de
alto rendimiento, redes y bases de datos que son propiedad y están administrados por diferentes instituciones.
Puesto que la colaboración entre instituciones envuelve un intercambio de datos, o de tiempo de computación,
el propósito del Grid es facilitar la integración de recursos computacionales. Universidades, laboratorios de
investigación o empresas se asocian para formar grids utilizado ciertos tipos de software que implementan este
concepto.

En la computación Grid, las redes pueden ser vistas como una forma de computación distribuida donde un
“supercomputador virtual” está compuesto por una serie de computadores agrupados para realizar grandes
tareas.

156
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Definiciones:

 Un Sistema Grid es una colección de recursos distribuidos conectados por una red, ubicados en
diferentes dominios administrativos, accesibles a los usuarios y aplicaciones con el objetivo de reducir el
coste e incrementar el rendimiento.

 Una Aplicación Grid es una aplicación que trabaja en un entorno de Grid.

 El middleware del Grid es el software que facilita la escritura de aplicaciones de Grid y la gestión de su
infraestructura.

Principales características:

 Gran escala.
 Distribución geográfica.
 Heterogeneidad.
 Compartición de recursos.
 Múltiples dominios de administración.
 Coordinación de recursos.
 Acceso transparente.
 Acceso seguro.
 Acceso coherente.

 Capacidad de balanceo de sistemas: No habría necesidad de calcular la capacidad de los sistemas en


función de los picos de trabajo, ya que la capacidad se puede reasignar desde la granja de recursos hacia
donde se necesite.
 Alta disponibilidad: Si un servidor falla, se reasignan los servicios en los servidores restantes.
 Reducción de costes: Los servicios son gestionados por "granjas de recursos". Ya no es necesario
disponer de grandes servidores y se puede hacer uso de componentes de bajo coste. Cada sistema puede
ser configurado siguiendo el mismo patrón;

El concepto de Grid se relaciona con la nueva generación del protocolo IP. El nuevo protocolo de Internet IPv6
permitirá trabajar con una Internet más rápida y accesible. Una de las ideas clave en la superación de las
limitaciones actuales de Internet IPv4 es la aparición de nuevos niveles de servicio que harán uso de la nueva
capacidad de la red para intercomunicar los ordenadores.

Este avance en la comunicación permitirá el avance de las ideas de Grid Computing al utilizar como soporte la
altísima conectividad de Internet. Es por ello que uno de los campos de mayor innovación en el uso del Grid
Computing, fuera de los conceptos de supercomputación, es el desarrollo de un estándar para definir los Grid
Services frente a los actuales Web Services.

En definitiva, el Grid Computing supone un avance respecto a la World Wide Web, que proporciona un acceso
transparente a la información que está almacenada en millones de ordenadores repartidos por todo el mundo.
Frente a ello, el Grid Computing es una infraestructura nueva que proporciona acceso transparente a potencia
de cálculo y capacidad de almacenamiento distribuida por una organización o por todo el mundo.

Un requisito que cualquier Grid debe cumplir es que los datos deben compartirse entre miles de usuarios con
intereses distintos. Se deben enlazar los centros principales de supercomputación, no sólo los PC. Se debe
asegurar que los datos sean accesibles en cualquier lugar y en cualquier momento. Debe armonizar las distintas
políticas de gestión de muchos centros diferentes. Debe proporcionar seguridad implícita.

157
Beneficios que se obtienen:

 Proporciona un mecanismo de colaboración transparente entre grupos dispersos, tales como científicos,
comerciales, etc.
 Posibilita el funcionamiento de aplicaciones a gran escala.
 Facilita el acceso a recursos distribuidos desde nuestros propios PC.
 Todos estos objetivos y beneficios se engloban en la idea de "e-Ciencia".

Estos beneficios tendrán repercusión en muchos campos:

- Medicina: Imágenes, diagnosis y tratamiento.


- Bioinformática: Estudios en genómica y proteómica.
- Nanotecnología: Diseño de nuevos materiales a escala molecular.
- Ingeniería: Diseño, simulación, análisis de fallos y acceso remoto a instrumentos de control.
- Recursos naturales y medio ambiente: Previsión meteorológica, observación del planeta, modelos y
predicción de sistemas complejos.

La tecnología derivada del Grid Computing abre un enorme abanico de posibilidades para el desarrollo de
aplicaciones en muchos sectores, como el desarrollo científico y tecnológico, educación, sanidad, y
administración pública.

Principales utilidades:

- Supercomputación distribuida.
- Computación de alto rendimiento.
- Computación bajo demanda.
- Computación intensiva.
- Computación colaborativa.

Comparación entre las Grid y los supercomputadores convencionales:

La computación distribuida o “Grid”, en general, es un tipo especial de computación paralela que se basa en
computadores completos (con CPUs, almacenamiento, fuentes de alimentación, interfaces de red, etc.)
conectados a una red (privada, pública o Internet) por una interfaz de red convencional aportando un hardware
sencillo, en comparación al diseño y construcción de baja eficiencia de un pequeño número de
supercomputadores personalizados. La principal desventaja en cuanto a rendimiento es que los diversos
procesadores y áreas locales de almacenamiento no tienen conexiones de alta velocidad. Esta composición es
apropiada para aplicaciones donde pueden darse lugar múltiples computaciones paralelas independientes, sin
la necesidad de comunicar resultados inmediatos entre procesadores.

Existen también algunas diferencias en la programación y en el despliegue. La escritura de programas que se


ejecuten en el entorno de un supercomputador puede resultar costosa y compleja, la cual puede tener un
sistema operativo personalizado, o incluso requerir de un programa para lidiar con los problemas de
concurrencia. Si se puede paralelizar un problema adecuadamente, una capa “delgada” de infraestructura Grid
puede permitir que programas convencionales e independientes se ejecuten en múltiples máquinas. Esto hace
posible escribir y depurar sobre una sola máquina convencional, y eliminar complicaciones debidas a múltiples
instancias de un mismo programa ejecutándose en la misma memoria compartida y un mismo espacio de
almacenamiento simultáneamente.

158
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.8.2 Tipos de Grids.

IntraGrids InterGrid
Una misma organización. Múltiples organizaciones.
Recursos heterogéneos. Múltiples dominios de administración.
Conexión LAN. Conexión usando WAN.
Menos problemas de seguridad. La seguridad es un aspecto muy importante.
Menos problemas de fiabilidad. Problemas de fiabilidad.

IntraGrid.

Una topología típica intraGrid, como se ilustra en la figura, puede existir dentro de una única empresa,
proporcionando un conjunto básico de servicios de la red. La única organización podría estar compuesta por una
serie de equipos que comparten un dominio de seguridad común y compartir datos internamente en una red
privada. Las características principales de una intraGrid son un único proveedor de seguridad, ancho de banda
alto en la red privada y alta disponibilidad, y contar con un único entorno dentro de la única red. Dentro de una
intraGrid es más fácil diseñar y operar las redes computacionales y de datos. Una intraGrid proporciona un
conjunto relativamente estable de los recursos informáticos y la capacidad de compartir fácilmente datos entre
sistemas de redes. La empresa podría considerar una intraGrid apropiada si tiene iniciativas para la reducción
económica a gran escala en la gestión interna del trabajo, o quiere empezar a explorar el uso de la red interna
primeramente al habilitar las aplicaciones empresariales verticales.

159
ExtraGrid.

Sobre la base de una única organización, la extraGrid amplía el concepto al reunir dos o más intraGrids. Una
extraGrid, como se ilustra en la figura superior, normalmente implica más de un proveedor de seguridad, y el
nivel de gestión de la complejidad aumenta. Las características principales de una extraGrid se dispersan en
seguridad, múltiples organizaciones, y la conectividad remota. Dentro de una extraGrid, los recursos se vuelven
más dinámicos y su Grid tiene que ser más reactiva a los recursos y componentes que han fallado. El diseño se
vuelve más complicado y los servicios de información se convierten en relevantes para asegurar que los recursos
de la red tengan acceso a la gestión de carga de trabajo en tiempo de ejecución.

Las empresas se benefician de las extraGrids si existe una iniciativa empresarial para la integración con socios de
negocios externos que sean de confianza. Una extraGrid también podría ser utilizada para B2B y/o para
establecer relaciones de confianza.

InterGrid.

Una interGrid requiere la integración dinámica de aplicaciones, recursos y servicios con los proveedores, clientes
y otras organizaciones autorizadas que obtienen acceso a la red a través de Internet / WAN. Una topología de
interGrid, como se ilustra en la figura, se utiliza principalmente por empresas de ingeniería, industrias científicas,
y empresas del sector financiero. Las características principales de una interGrid incluyen seguridad, múltiples
organizaciones, y la conectividad remota dispersa. Los datos en una red interconectada son datos globales
públicos y aplicaciones, tanto vertical como horizontal, deben ser modificados para una audiencia global. Una
empresa puede asimilar la necesidad de una interGrid si existe una necesidad de computación peer-to-peer, una
comunidad de computación colaborativa, o para realizar procesos con las organizaciones que utilizan la interGrid.

160
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.8.3 Arquitectura del Grid.

Los diferentes componentes del Grid se organizan en capas, donde en cada capa se construye con los servicios
ofrecidos por la capa inferior, e interactuando con los componentes de su misma capa.

Organización típica de la arquitectura de un Grid:

 Fábrica.

Suministra los recursos, tales como:

- Ordenadores: Pueden ser tanto clústeres, como supercomputadores o PCs, además pueden ejecutar
diferente ssistemas operativos.
- Entornos de ejecución:
- Condor (http://www.cs.wisc.edu/condor/).
- SunGridEngine (http://gridengine.sunsource.net/ para la versión libre y
http://www.sun.com/software/gridware/ para la versión comercial).
- Torque (http://www.clusterresources.com/pages/products/torque-resource-manager.php)
- Redes de comunicaciones.
- Dispositivos de almacenamiento.
- Instrumental científico.

161
 Núcleo del Middleware Grid.

Suministra servicios tales como:


- Gestión de procesos remotos.
- Acceso al almacenamiento.
- Descubrimiento en registro de información.
- Seguridad.
- Apoyo para ofrecer calidad de servicio (QoS) como reserva y adquisición de recursos.

Abstrae la complejidad y heterogeneidad del nivel de fábrica.


Proporciona un método consistente para acceder a los recursos distribuidos.

 Nivel usuario del Middleware Grid.

Proporciona abstracciones y servicios de más alto nivel en entornos de desarrollo de aplicaciones y para
herramientas de programación.

 Aplicaciones Grid y portales.

5.8.4 Proyecto Globus.

https://www.globus.org/

La Globus Alliance es una asociación internacional dedicada a desarrollar tecnologías necesarias para construir
infraestructuras de computación grid.

El proyecto Globus se define como un conjunto de herramientas de código abierto para construir infraestructuras
Grid. Permite la compartición de capacidad de proceso, de bases de datos y otros recursos de manera segura,
atravesando diferentes límites corporativos, institucionales o geográficos, sin sacrificar la autonomía local.
Donde los usuarios mantienen el control local de los propios recursos.

En 1995 se celebró el congreso SuperComputing’95, donde se demostró que era posible ejecutar aplicaciones
distribuidas de varias áreas científicas entre 17 centros de Estados Unidos conectados por una red de alta
velocidad de 155 Mbps. Este experimento se denominó I-Way, y fue el punto de partida de varios proyectos en
diferentes áreas, con un denominador común que era la compartición de recursos distribuidos de computación.
A partir de este momento, el libro “The Grid: Blueprint for a New Computing Instraestructure”, editado por Ian
Foster y Carl Kesselman supuso el primer paso para establecer unas primeras ideas claras sobre cómo debía
llevarse a cabo esta nueva tecnología.

A partir de estas ideas se desarrolló surgió el Globus toolkit, un proyecto open-source desarrollado en el Argonne
Nacional Laboratory dirigido por Ian Foster en colaboración con el grupo de Carl Kesselman de la Universidad de
Southern California. Globus da los medios básicos de la tecnología para construir un Grid computacional, y se ha
convertido gracias a su evolución y adopción por la comunidad científica como el estándar de facto en la
tecnología Grid.

162
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

La arquitectura abierta de Globus se estructura en capas, siguiendo los estándares propuestos por Foster y
Kesselman e incluye servicios software para la monitorización de recursos, descubrimiento y gestión, además de
servicios de seguridad y de gestión de ficheros. Incluye software organizado en áreas como seguridad,
infraestructura de la información, detección de fallos, portabilidad, etc. Está empaquetado como una serie de
componentes que pueden usurase bien independientemente o conjuntamente para desarrollar aplicaciones.

El Globus toolkit fue concebido para eliminar los obstáculos que impide la colaboración entre diferentes
organizaciones o instituciones. Sus servicios centrales (core services), interfaces y protocolos permiten a los
usuarios acceder a los recursos remotos como si estuvieran presentes dentro de su propia sala de máquinas, a
la vez que preservan el control local sobre quién y cuándo puede usar los recursos.

El reloj de arena de Globus.

Los elementos del Globus toolkit no asumen que los entornos locales estén
adaptados para soportarlo. En principio fue diseñado e implementado para adaptar
a los muchos y varios entornos locales bajo el cual pueda ejecutarse.

Globus ofrece una serie de servicios básicos para establecer una infraestructura
básica. Éstos son luego usados para construir soluciones específicas de cada
dominio, de alto nivel. Para ello tres principios de diseño clave que se siguen son
mantener los costes de participación bajos, mantener el control local cuando quiera
sea posible, y proveer soporte para la adaptación del toolkit a las necesidades
específicas de cada sitio y cada proyecto.

La base del reloj de arena de Globus representa los miles de recursos sobre los que los servicios de Globus han
sido construidos. Por ejemplo, los sistemas operativos locales de las varias máquinas sobre las que corre Globus,
además de los muchos tipos de redes, los sistemas de planificación, los sistemas de ficheros, etc. El medio es
comprimido por los servicios básicos (core services) que Globus ofrece, y la parte de arriba representa los
servicios de alto nivel que Globus ofrece, así como las propias aplicaciones escritas en Globus.

Las implementaciones locales de los servicios de Globus para un sistema operativo en particular liberan tanto a
los servicios básicos como a los servicios de alto nivel de tener que conocer cuestiones específicas del sistema
operativo. Sólo los servicios locales necesitan conocer cuál es el sistema operativo sobre el que funciona,
liberando a los programadores de aplicaciones de conocer estos detalles.

Una visión más detallada de los servicios ofrecidos por Globus puede ser representado en la siguiente figura:

163
Capas de la arquitectura Globus:

La capa superior contiene herramientas que integran los servicios de la capa inferior o los complementa.

La capa que contiene el núcleo de Globus está formada por los siguientes servicios:

 Capa de seguridad GSI (GSI Security Layer). Métodos para autenticar a los usuarios y hacer que las
comunicaciones seguras.
 Gestión de Recursos (Grid Resource Management [GRAM, GASS]). Para la asignación de recursos, tales
como el envío de trabajos a ejecutar, monitorización de trabajos y recogida de resultados.

 Servicios de Información (Grid Information Services [MDS]). Proporciona propiedades dinámicas y


estáticas de los nodos que están conectados.

 Gestión de Datos (Grid Data Management [Grid FTP, Replica Catalog]). Proporciona utilidades y librerías
para transmitir, almacenar y gestionar grandes volúmenes de datos necesarios para las aplicaciones que
se ejecutan.

La capa de recursos y servicios locales (Grid Resource and Local Services) contiene:

- Los servicios del sistema operativo.


- Los servicios de red, como por ejemplo TCP/IP.
- Los servicios de planificación de clúster, como el envío de tareas y la consulta de colas.

164
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.8.5 Desktop Grid.

Agregan la capacidad computacional de los extremos de Internet. Por extremos de Internet entendemos todos
aquellos ordenadores de usuarios conectados a la red. Establece un middleware que permite a estos
ordenadores interconectarse y crear una red que agrega las capacidades de computación no usadas.

Aparece el proyecto de Infraestructura Abierta de Berkeley para la Computación en Red (BOINC, Berkeley Open
Infrastructure for Network Computing) como la tecnología más relevante en este área. Se trata de una
infraestructura para la computación distribuida, desarrollada originalmente para el proyecto SETI@home, pero
que actualmente se utiliza para diversos campos como física, medicina nuclear, climatología, etc. La intención de
este proyecto es obtener una capacidad de computación enorme utilizando computadores personales alrededor
del mundo. Los proyectos en los que trabaja este software tienen un denominador común, y es que requieren
una gran capacidad de cálculo.

BOINC ha sido desarrollado por un equipo ubicado en Space Sciences Laboratory en la Universidad de California
en Berkeley, liderado por David P. Anderson, que también lidera SETI@home. Como una plataforma "quasi-
supercomputador", BOINC tiene alrededor de 527.880 computadores activos (hosts) alrededor del mundo
procesando en promedio 5.549 petaFLOPS (el 11 de marzo de 2011), que supera el poder de cómputo del
supercomputador más rápido existente, el Tianhe-I de China, con un ritmo de procesamiento de 2.566
petaFLOPS.

Ejemplos de proyectos basados en BOINC:

Busca ondas gravitacionales en los datos recogidos por


Einstein@home LIGO
varios observatorios.
Rosetta@home U. Washington Predicción de la estructura de las proteínas.
SETI@home U.C. Berkeley; SETI.
LHC@home CERN Simulación del acelerador de partículas.
STI, U. of Varios proyectos como la simulación de la dinámica de
Africa@home
Geneva; la transmisión y los efectos en la salud de la malaria.
IBM Investigación de enfermedades, desastres naturales,
WorldCommunityGrid problemas medioambientales, entre otros.

165
5.9 Utility Computing.

Utility computing se define como el suministro de recursos computacionales, como puede ser el procesamiento
y almacenamiento, como un servicio medido similar a las utilidades públicas tradicionales (como la electricidad,
el agua, el gas natural o el teléfono).

Este sistema tiene la ventaja de tener un costo nulo o muy bajo para adquirir hardware; en cambio, los recursos
computacionales son esencialmente alquilados. Los clientes que realizan procesamiento de datos a gran escala
o que están frente a un pico de demanda también pueden evitar los atrasos que resultarían de adquirir y
ensamblar físicamente una gran cantidad de computadoras.

Esta nueva presentación de los servicios de computación se convirtió en el fundamento del cambio como
"on demand computing", software como servicio y modelos de Cloud Computing que propagó aún más la idea
de la computación, la aplicación y la red como un servicio. Había un poco de escepticismo inicial acerca de un
cambio tan significativo. Sin embargo, el nuevo modelo de computación tuvo éxito y finalmente se convirtió en
la corriente principal.

IBM, HP y Microsoft fueron los primeros líderes en el nuevo campo de la Utility Computing con sus unidades de
negocio y los investigadores que trabajan en la arquitectura, el pago y el desarrollo, desafíos del nuevo modelo
de computación. Google, Amazon y otros comenzaron a tomar la iniciativa en 2008, cuando establecieron sus
propios servicios de Utility Computing, de almacenamiento y aplicaciones.

La Utility Computing puede apoyar la computación Grid que tiene la característica de grandes cálculos o
repentinos picos de demanda que son apoyados a través de un gran número de equipos. La Utility Computing,
por lo general está provisto algún tipo de virtualización de modo que la cantidad de almacenamiento o de la
potencia de cálculo disponible es considerablemente mayor que la de un solo equipo en tiempo compartido. Se
utilizan varios servidores en el "back-end" para hacerlo posible. Estos pueden ser un clúster de ordenadores
dedicados construido específicamente para el propósito de ser alquilados, o incluso un superordenador
infrautilizado. La ejecución de un solo cálculo en varios equipos se conoce como computación distribuida.

Empresas con exceso de capacidad de cómputo pueden, de forma rentable, dejar usar sus sistemas a distintos
clientes. Por lo que otras empresas, con demanda de capacidad de cómputo, pueden alquilar la infraestructura
de quién le ofrezca mejor precio o servicio (o relación entre ellos). Así pues:

 No hay que pagar por construir grandes centros de datos.


 No hay que pagar por la compleja administración de sistemas.
 No hay que pagar el elevado consumo eléctrico.

166
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.10 Cloud Computing.

La computación en la nube, conocido también como servicios en la nube, informática en la nube, nube de
cómputo o nube de conceptos, es un paradigma que permite ofrecer servicios de computación a través de
Internet.

En este tipo de computación todo lo que puede ofrecer un sistema informático se ofrece como servicio, de modo
que los usuarios puedan acceder a los servicios disponibles "en la nube de Internet" sin conocimientos (o, al
menos sin ser expertos) en la gestión de los recursos que usan. Según el IEEE Computer Society, es un paradigma
en el que la información se almacena de manera permanente en servidores de Internet y se envía a cachés
temporales de cliente, lo que incluye equipos de escritorio, centros de ocio, portátiles, etc.

La computación en la nube son servidores de Internet encargados de atender las peticiones en cualquier
momento. Se puede tener acceso a su información o servicio, mediante una conexión a internet desde cualquier
dispositivo móvil o fijo ubicado en cualquier lugar. Sirven a sus usuarios desde varios proveedores de alojamiento
repartidos por todo el mundo. Esta medida reduce los costos, garantiza un mejor tiempo de actividad y que los
sitios web sean invulnerables a los delincuentes informáticos, a los gobiernos locales y a sus redadas policiales.

El Cloud Computing es un nuevo modelo de prestación de servicios de negocio y tecnología, que permite incluso
al usuario acceder a un catálogo de servicios estandarizados y responder con ellos a las necesidades de su
negocio, de forma flexible y adaptativa, en caso de demandas no previsibles o de picos de trabajo, pagando
únicamente por el consumo efectuado, o incluso gratuitamente en caso de proveedores que se financian
mediante publicidad o de organizaciones sin ánimo de lucro.

El cambio que ofrece la computación desde la nube es que permite aumentar el número de servicios basados
en la red. Esto genera beneficios tanto para los proveedores, que pueden ofrecer, de forma más rápida y
eficiente, un mayor número de servicios, como para los usuarios que tienen la posibilidad de acceder a ellos,
disfrutando de la ‘transparencia’ e inmediatez del sistema y de un modelo de pago por consumo. Así, el
consumidor ahorra en costes salariales o en costes en inversión económica (locales, material especializado, etc.).

La computación en nube consigue aportar estas ventajas, apoyándose sobre una infraestructura tecnológica
dinámica que se caracteriza, entre otros factores, por un alto grado de automatización, una rápida movilización
de los recursos, una elevada capacidad de adaptación para atender a una demanda variable, así como
virtualización avanzada a un precio flexible en función del consumo realizado, evitando además el uso
fraudulento del software y la piratería.

La computación en nube es un concepto que incorpora el software como servicio, como en la Web 2.0 y otros
conceptos recientes, también conocidos como tendencias tecnológicas, que tienen en común el que confían en
Internet para satisfacer las necesidades de cómputo de los usuarios.

Por tanto, se trata de un modelo que permite el acceso a través de la red a un conjunto compartido de recursos
informáticos configurables (redes, almacenamiento, aplicaciones y servicios) que pueden ser rápidamente
provisionados y liberados con un esfuerzo mínimo de gestión o interacción con el proveedor de servicios.

Sus principales retos son:

 Disponibilidad.
 Tolerancia a fallos.
 Escalabilidad.
 Seguridad.
 Conectividad.

167
Motivación:

La computación en nube permite acceder a todas sus aplicaciones y documentos desde cualquier lugar del
mundo, dándole por supuesto mucha más libertad que la que le dan las paredes de una empresa o los bordes
de una mesa de trabajo, además facilita la colaboración en grupo al por mayor. Sin embargo, la computación en
la nube no es para todos, hay pros y contras de este tipo de informática que se basa totalmente en Internet.

Este tipo de procedimientos basados en Internet nos libera de la tiranía de la computación de escritorio y abre
nuevas formas de colaboración en grupo. Pero por más atractivo que parezca, la computación en la nube no es
para todos. En general, el usuario final medio se puede beneficiarse de ello, pero algunos usuarios deben evitar
estas aplicaciones basadas en web, al menos por ahora.

Ventajas:

 Menor gasto de informática. No necesitamos un ordenador de alta potencia y alto precio para procesar
aplicaciones basadas en web que están en la nube. Dado que las aplicaciones se ejecutan en la nube y
no en el ordenador de escritorio, no necesitamos la potencia de procesamiento o de espacio en disco
duro exigida por el software de escritorio tradicional. Cuando se está utilizando aplicaciones basadas en
web, nuestro ordenador puede ser uno más económico, con un disco duro más pequeño, menos
memoria y procesador. De hecho, un ordenador en este escenario no necesitamos ni siquiera una unidad
de CD o DVD, ya que no hay programas que tengan que ser instalados y los datos o los documentos no
necesitan ser salvados en el disco duro.

 Mejora del rendimiento. Con un menor número de programas acaparando la memoria de nuestro
ordenador, veremos un mejor rendimiento. En pocas palabras, los ordenadores que utilicen aplicaciones
de la nube correrán más rápido porque tienen menos programas y procesos cargados en memoria.

 Reducción del coste en software. En lugar de comprar costosas aplicaciones de software, podemos
conseguir casi todo lo que necesitamos de forma gratuita o a costes muy bajos. Así es, la mayoría de las
aplicaciones de computación en la nube de hoy, o bien son totalmente gratuitas o tienen costes muy
bajos. Lo cual es mejor que pagar los costes de licencia como el de alguno de los conocidos "suite de
oficina" o la compra de software en general.
168
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

 Las actualizaciones de software son instantáneas. Otra de las ventajas del software en la nube es que
estás ya no se enfrentan a la elección entre software obsoleto y los altos costes de actualización. Cuando
la aplicación está basada en la web, las actualizaciones desde el punto de vista del usuario, se hacen en
forma automática y simplemente estarán disponibles la próxima vez que iniciemos sesión en la nube. Al
acceder a una aplicación basada en web, tendremos siempre la última versión sin necesidad de pagar o
descargar e instalar actualizaciones.

 Capacidad de almacenamiento casi ilimitada. La computación en nube ofrece un almacenamiento


prácticamente ilimitado. Por ejemplo, si nuestro ordenador actual tiene 200 GigaBytes de disco duro,
tengamos en cuenta que eso es infinitamente pequeño con las capacidades de los TB disponibles en la
nube. Lo que necesitaremos para almacenar, se puede redimensionar a petición y dinámicamente.

 Aumento de la fiabilidad de los datos. A diferencia de la computación de escritorio, en la que un fallo del
disco duro puede destruir todos nuestros valiosos datos, si el disco de nuestro ordenador simplemente
deja de funcionar no afectaría a nuestros datos. O si el ordenador se bloquea, todos los datos seguirán
en la nube, todavía accesibles. En un mundo donde pocos usuarios individuales de ordenadores hacen
copias de seguridad de sus datos en forma regular, en la computación en la nube se utilizan las últimas
tecnologías de copias de seguridad, y redundancia de discos duros, o sea lo último en seguridad de datos,
por lo que no debemos preocuparnos por realizar las copias de seguridad, la nube las realizará
automáticamente.

 Acceso universal a los documentos. ¿No le ha pasado alguna vez que necesita recordar o acceder a algún
dato y no puede hacerlo porque no está en la oficina? Eso no es un problema con el Cloud Computing,
ya que los datos que están en la nube pueden accederse desde cualquier ordenador con conexión a
internet. Todos los datos están disponibles al instante desde cualquier lugar, simplemente no hay
necesidad de llevar sus documentos con nosotros.

 Disponibilidad de la última versión. Otra de las ventajas en el caso de documentos relacionados con la
computación en nube es que cuando se edita un documento en algún lugar, la versión editada será lo
que verá al acceder al documento en cualquier otro lugar. La nube siempre se aloja la última versión de
los documentos, siempre y cuando estemos conectado.

 Facilitar la colaboración en grupo. Compartir documentos conduce directamente a la colaboración en


documentos. Para muchos usuarios, esta es una de las ventajas más importantes de la computación en
nube, múltiples usuarios pueden colaborar fácilmente en documentos y proyectos. Debido a que los
documentos están alojados en la nube, no en equipos individuales, todo lo que necesitamos es un
ordenador con conexión a Internet.

 Independencia del dispositivo. Finalmente, aquí está una de las ventajas más importantes de la
computación en nube es que ya no estamos atado a un solo ordenador. Podemos cambiar los
ordenadores, que las aplicaciones y los datos seguirán estando en la nube. Podemos utilizar un
dispositivo portátil, y sus aplicaciones y los datos estarán todavía disponibles. No hay necesidad de
comprar un dispositivo específico, un sistema operativo especial o un programa para ese dispositivo en
particular. Los documentos, datos y aplicaciones son los mismos sin importar qué ordenador o
dispositivo que esté utilizando para acceder a ellos. Hoy por hoy se pueden utilizar ordenadores con
Windows, Linux, Mac OS/X, tabletas o Smart Phones, etc. y pronto seguramente habrá muchos más
dispositivos y marcas en el mercado, todos con un objetivo común: poder acceder a la nube.

169
Desventajas:

Hay una serie de razones por las que es posible que haya usuarios no quieran adoptar la computación en la nube
para sus necesidades particulares. Vamos a examinar algunos de los riesgos relacionados con la computación en
la nube:

 Se requiere una conexión permanente a Internet. La computación en la nube es imposible si no se puede


conectar a Internet. Dado que se utiliza Internet para conectarse a las aplicaciones y por tanto a los datos
y documentos, si no tuviéramos una conexión a Internet no podremos acceder a nada que esté en la
nube. En el período que una conexión a Internet esté caída no podremos trabajar con las aplicaciones ni
acceder a los datos, en las zonas donde las conexiones a Internet son de mala calidad o poco fiables.
Cuando no estemos conectado, la computación en la nube, simplemente no funciona.

 No funciona bien con conexiones de baja velocidad. Del mismo modo, una conexión a Internet de baja
velocidad, hace que la computación en nube sea en muchos casos imposible. Las aplicaciones basadas
en web requieren una gran cantidad de ancho de banda, al igual que para trabajar con documentos de
gran tamaño. En otras palabras, la computación en la nube funcionará correctamente siempre que la
velocidad de acceso sea suficientemente buena.

 Algunas veces puede ser demasiado lento. Incluso con una conexión rápida, las aplicaciones basadas en
web seguramente serán más lentas que aplicaciones similares instaladas en nuestro ordenador de
escritorio. Esto se basa en muchas variables de las que depende el procesamiento en la nube, por
ejemplo cada actualización tiene que ser enviada de ida y vuelta desde nuestro ordenador hacia los
servidores en la nube. Si los servidores de la nube en ese momento están haciendo una copia de
seguridad, o si Internet está demasiado saturado (horas punta de utilización) nunca tendrá una respuesta
instantánea como suele pasar con las aplicaciones de escritorio.

 Los datos almacenados pueden no estar seguros. Con la computación en la nube, todos los datos se
almacenan en la nube. ¿Qué tan segura es la nube? ¿Pueden los usuarios no autorizados acceder a
nuestros datos confidenciales? Pueden las empresas que ofrecen servicios de computación en nube
decir que sus datos están seguros. Tal vez sea demasiado pronto para poder afirmar estar
completamente seguro de eso. Sólo el tiempo dirá si nuestros datos están seguros en la nube.

 Teóricamente siempre existirá la posibilidad de que los datos almacenados se puedan perder. La mayoría
de las empresas que brindan servicios de computación en la nube toman los recaudos suficientes para
que eso no ocurra, y aseguran que así será, por ejemplo instalando, líneas de datos redundantes
conectadas a firewalls físicos, sistemas de alimentación eléctrica ininterrumpidos, almacenamiento
tolerantes a fallos, servicios de copias de seguridad automáticos, almacenamiento de las copias de
seguridad en ambientes protegidos físicamente (contra incendios o robos), pero no obstante, al ser
medios físicos, nunca nos darán una seguridad del 100%, no nos olvidemos de lo que dijo el famoso
Murphy, todo lo que pueda fallar, fallará...

170
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.10.1 Tipos de Cloud Computing.

Como nota común a todos ellos, este tipo de servicios se factura en función de los recursos consumidos que
normalmente son el reflejo del nivel de actividad del sistema.

Software as a Service (SaaS)

Software como Servicio. Modelo de distribución de software donde una empresa sirve el
mantenimiento, soporte y operación que usará el cliente durante el tiempo que haya contratado el
servicio. El cliente usará el sistema alojado por esa empresa, la cual mantendrá la información del cliente
en sus sistemas y proveerá los recursos necesarios para explotar esa información.

Ejemplos: Google Apps, Microsoft “Software+Services”, Salesforce, Basecamp, …

Platform as a Service (PaaS)

Plataforma como Servicio. Aunque suele identificarse como una evolución de SaaS, es más bien un
modelo en el que se ofrece todo lo necesario para soportar el ciclo de vida completo de construcción y
puesta en marcha de aplicaciones y servicios web completamente disponibles en Internet. Otra
característica importante es que no hay descarga de software que instalar en los equipos de los
desarrolladores. PasS ofrece múltiples servicios, pero provisionados como solución integral en la web.

Ejemplos: Amazon Web Services (SimpleDB y SQS), Google App Engine, IBM IT Factory, Force.

Infrastructure as a Service (Iaas)

Infraestructura como Servicio. Modelo de distribución de infraestructura de computación como un


servicio, normalmente mediante una plataforma de virtualización. En vez de adquirir servidores, espacio
en un centro de datos o equipamiento de redes, los clientes compran todos estos recursos a un
proveedor de servicios externo. Una diferencia fundamental con el hosting virtual es que el
provisionamiento de estos servicios se hace de manera integral a través de la web.

Ejemplos: Amazon Web Services EC2, Sun Grid, IBM Blue Cloud, GoGrid, …

Storage as a Service (dSaaS)

Almacenamiento como servicio. Modelo de negocios en el cual una compañía renta espacio en su
infraestructura de almacenamiento a otra compañía más pequeña o individuo. A nivel corporativo, los
proveedores de SaaS apuntan a aplicaciones de almacenamiento secundario, promoviendo SaaS como
una forma conveniente de gestionar los respaldos. Generalmente es visto como una buena alternativa
para una pequeña o mediana empresa que carezca del presupuesto y/o del personal técnico para
implementar y mantener su propia infraestructura de almacenamiento.

Ejemplos: Amazon S3, Nirvanix SDN, Cleversafe dsNet, …

171
Main Access &
Service Class Service content
Management Tool
Cloud Applications
- Social networks.
- Office suites.
- CRM.
SaaS Web browser - Video processing.
- eMail.
- Virtual desktop.
- Communicacion.
- Games.
Cloud Platform
- Programming languages.
- Frameworks.
- Mashups editors.
PaaS Cloud Development Environment - Structured data.
- Execution runtime.
- Database.
- Web server.
- Development tolos.
Cloud infraestructura
- Compute servers.
- Data Storage.
- Firewall.
IaaS Virtual Infraestructure Manager
- Load balancer.
- Virtual machines.
- Storage servers.
- Networking.

Cloud clientes
Web browser, mobile app, thin client, terminal emulator, …

Características de los sistemas de gestión Cloud:

 Soporte de virtualización.
 Aprovisionamiento de recursos bajo demanda.
 Virtualización del almacenamiento.
 Interfaz con clouds públicos.
 Virtualización de la red.
 Asignación dinámica de los recursos.
 Alta disponibilidad y recuperación.

Ejemplo de gestores Cloud:

- Apache VCL.
- AppLogicCitrix Essentials.
- EnomalyECP.
- Eucaliptus.
- Nimbus3.
- OpenNebula.
- OpenPEX.
- VMWare vSphere and vCloud.

172
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5.10.2 Proveedores de infraestructura como servicio.

Características:

 Distribución geográfica.
 Interfaces de usuarios y accesos a servidores.
 Open Cloud Computing Interface Working Group (OCCIWG).
 Reserva adelantada de recursos.
 Escalabilidad y equilibrio de carga.
 Acuerdo de nivel de servicio.
 Elección de hypervisor y sistema operativo.

Ejemplos:

- Amazon Web Services.


- Flexiscale.
- Joyent.
- CoGrid.
- Rackspace.

Amazon EC2 (Elastic Compute Cloud)

o El servicio EC2 factura la capacidad de cómputo deseada.


o Instancias de máquinas virtuales desplegadas: Distintos tipos de instancias (pequeña, grande, etc.)
o Transferencia de Datos: Tarifa por los datos transferidos a/desde EC2.
o Almacenamiento: Tarifa por las peticiones sobre los ficheros (GET, PUT, LIST, etc.).
o Servicios adicionales: Almacenamiento, monitorización, direcciones IP, reparto de carga, etc.
o Usos diversos: Aplicaciones, Almacenamiento, distribución de contenido.

Amazon S3 (Simple Storage Service)

El servicio S3 factura por tres conceptos conjuntamente:

- Cantidad almacenada: Se tarifa por GB almacenado/mes. Cuanto más almacenemos, más pagamos.
- Transferencia de Datos: Se tarifa por los GB transferidos. Cuanto más transfiramos, más pagamos.
- Peticiones de acceso: Se tarifa por las peticiones sobre los ficheros (GET, PUT, LIST, etc.).

A pesar de todo, los servicios de Amazon son competitivos, ya que unos 2’5 GB de datos almacenados y con una
transferencia de 15 GB al mes, no llegarán a los 4 dólares (2’69 €) mensuales.
173
5.11 Máquinas virtuales.

Las Máquinas virtuales posibilitan de ejecutar en un computador (host) un programa que crea un computador
virtual (guest) sobre el que ejecutar cualquier entorno.

Hypervisor: Software que permite que múltiples sistemas operativos puedan ejecutarse en un mismo
computador concurrentemente. Tenemos dos tipos de hipervisores: los que ejecutan directamente sobre el HW
(XenServer, XenClient, KVM, etc.) y los que ejecutan sobre un sistema operativo (Virtual Server, VMware, Virtual
BOX, Virtual PC, etc.).

Los Web Sites son la opción más adecuada para la mayoría de aplicaciones web. La implementación y la
administración están integradas en la plataforma, los sitios pueden escalarse rápidamente para asumir altas
cargas de tráfico y el equilibrio de carga y el administrador de tráfico incluido ofrecen una gran disponibilidad.
Se pueden mover los sitios actuales a Web Sites fácilmente con una herramienta de migración en línea, utilizar
una aplicación de código abierto de la galería de aplicaciones web o crear un sitio nuevo utilizando el marco y
las herramientas que se prefiera.

Pero si se necesita más control sobre el entorno del servidor web, como la posibilidad de tener acceso remoto
al servidor o configurar las tareas de inicio del servidor, los servicios en la nube son por lo general la mejor opción.

Si actualmente se tiene una aplicación que requeriría cambios sustanciales para ejecutarse en Web Site, se podría
elegir el uso de Máquinas virtuales a fin de simplificar la migración a la nube. Sin embargo, se requiere más
tiempo para configurar, proteger y mantener adecuadamente las Máquinas virtuales, además de mayores
conocimientos informáticos, en comparación con un Web Site y otros Cloud Services. Cuando se está
considerando la opción de Máquinas virtuales, hay que tener en cuenta el esfuerzo constante de mantenimiento
requerido para aplicar revisiones al entorno de la Máquina virtual, así como para actualizarlo y administrarlo.

El diagrama siguiente ilustra el control relativo frente a la facilidad de uso para cada una de estas opciones:

174
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Prácticas de Laboratorio.

Práctica 0 – Conceptos básicos.

P1. (Problema extraído de Arquitectura de Computadores, Un Enfoque Cuantitativo, Morgan Kaufmann 2012.
Hennesy Patterson, 5ª edición) Un desafío para los arquitectos es que el diseño creado hoy requerirá varios años
de implementación, verificación y pruebas antes de aparecer en el mercado. Lo que significa que debe
proyectarse hacia lo que la tecnología llegará a ser con varios años de antelación. A veces, esto es difícil de hacer.

a) De acuerdo con la tendencia de escalamiento observado por la ley de Moore (aprox. 35% por año),
¿Cuántas veces mayor debería ser el número de transistores en un chip en 2015 respecto a 2005?

b) Esta tendencia refleja el aumento de las velocidades de reloj. Si las velocidades de reloj continúan
subiendo al mismo ritmo que en la década de 1990, ¿qué valor tendría aproximadamente las velocidades
de reloj en el año 2015, suponiendo que era de 3’2 GHz en 2003?

c) La tasa actual de crecimiento (1% por año) ¿Cuáles son las velocidades de reloj proyectadas ahora para
el 2015?

d) ¿Qué ha limitado la tasa de crecimiento de la velocidad de reloj? ¿Qué hacen los arquitectos de hardware
con los transistores adicionales para aumentar el rendimiento?

Solución:

Apartado a)

Si X2005 = X2004 ∙ 1’35; X2006 = X2005 ∙ 1’35; X2007 = X2006 ∙ 1’35 …


Entonces X2015 = X2014 ∙ 1’35 ⇒ X2015 = X2005 ∙ (1’35)(2015-2005)
Por tanto, X2015 = X2005 ∙ (1’35)10

Apartado b)

Si 2003 = a1, 2004 = a2, . . . , 2015 = a13


Entonces Tomamos hasta el término a12 pues suponemos que 2015 aún estaría en curso.
Por tanto, X2015 = 3’2 GHz ∙ (1’35)12 = 117 GHz

Apartado c)

X2015 → término a13 de la sucesión


X2015 = 3’2 GHz ∙ (1’01)12 = 3’6 GHz

Apartado d)

Al disminuir el voltaje se disminuye el umbral que definen los estados (0 y 1) llegando a su límite
físico. Se opta por paralelizar los procesos aumentando el número de procesadores.
175
P2. (Problema extraído de Arquitectura de Computadores, Un Enfoque Cuantitativo, Morgan Kaufmann 2012.
Hennesy Patterson, 5ª edición) suponga que está diseñando un sistema para una aplicación en tiempo real que
se deben ejecutarse en unos tiempos específicos. Acabado el cálculo más rápido observa que no se gana nada.
Posteriormente encuentra que su sistema puede ejecutar el código requerido, en el peor de los casos, dos veces
más rápido que el inicial.

a) ¿Cuánta energía se ahorra si se ejecuta a la velocidad actual (dos veces más rápido) y se apaga el sistema
cuando el proceso de cálculo se ha completado?

b) ¿Cuánta energía se ahorra si se establece la tensión y la frecuencia a la mitad?

Solución:

De partida sabemos lo siguiente:

Potencia = Carga capacitiva x Tensión x Frecuencia → P = k ∙ v2 ∙ f


Energía = Potencia consumida instantánea x Tiempo → E = P ∙ t ⇒ E = (k ∙ v2 ∙ f) ∙ t

Apartado a)

Potencia Potencia

Energía
Energía consumida consumida

Tiempo Tiempo
Caso Inicial: E0 = P ∙ t Caso Mejorado: Em = P ∙ t/2
Dos veces más rápido es reducir el tiempo a la mitad.
» Por tanto, si el proceso se ejecuta en la mitad de tiempo se ahorra la mitad de la potencia inicial.

Apartado b) Potencia

Energía consumida

Tiempo
Caso Mejorado (sólo la frecuencia): Ef = k ∙ V2 ∙ f/2 ∙ t∙2
Reducir la frecuencia a la mitad supone emplear el doble de tiempo en ejecución.
Luego reducir la frecuencia implica seguir consumiendo la misma cantidad de energía.

Potencia

Tiempo
Caso Mejorado (tensión y frecuencia): Ev f = k ∙ (V/2)2 ∙ f/2 ∙ t∙2 = k ∙ V/4 ∙ f ∙ t
Reducir la frecuencia a la mitad supone emplear el doble de tiempo en ejecución.
Luego reducir además la tensión implica reducir la energía consumida a la cuarta parte.

» Por tanto, al bajar la tensión se disminuye determinantemente el consumo energético.


176
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P3. (Problema extraído de Arquitectura de Computadores, Un Enfoque Cuantitativo, Morgan Kaufmann 2012.
Hennesy Patterson, 5ª edición). Usando el juego de instrucciones de la Figura A.27, calcular el CPI efectivo para
instrucciones enteras para el procesador MIPS. Supongamos que el 60% de las bifurcaciones condicionales se
toman. También asumimos que hemos tomado las siguientes medidas de CPI promedio para ese tipo de
instrucciones:

Figura A.27. Conjunto de instrucciones dinámicas del MIPS para cinco programas SPECint2000. Tenga en cuenta
que las instrucciones de movimiento de registro a registro entero están incluidas en la instrucción OR. Las
entradas en blanco tienen el valor de 0.0%.

177
Solución:

𝑎𝑑𝑑 19%
𝑠𝑢𝑏 3%
𝑚𝑢𝑙 0%
𝑐𝑜𝑚𝑝𝑎𝑟𝑒 5%
𝑙𝑜𝑎𝑑 𝑖𝑛𝑚 2%
𝐶𝑃𝐼𝑒𝑓𝑒𝑐𝑡𝑖𝑣𝑜 = ∙ (1.0 𝐶𝑃𝐼𝐴𝑙𝑙 𝐴𝐿𝑈 𝑖𝑛𝑠𝑡𝑢𝑐𝑡𝑖𝑜𝑛𝑠 )
𝑠ℎ𝑖𝑓𝑡 2%
𝐴𝑁𝐷 4%
𝑂𝑅 9%
𝑋𝑂𝑅 3%
(𝑜𝑡ℎ𝑒𝑟 𝑙𝑜𝑔𝑖𝑐𝑎𝑙
⏟ 0%)
∑ 48%
𝑙𝑜𝑎𝑑 26%
+( ) ∙ (1.4 𝐶𝑃𝐼𝐿𝑜𝑎𝑑𝑠−𝑠𝑡𝑜𝑟𝑒𝑠 )
⏟𝑠𝑡𝑜𝑟𝑒 10%
∑ 36%
𝑗𝑢𝑚𝑝 1%
+ (𝑐𝑎𝑙𝑙 1%) ∙ (1.2 𝐶𝑃𝐼𝐽𝑢𝑚𝑝𝑠 )
⏟𝑟𝑒𝑡𝑢𝑟𝑛 1%
∑ 3%
𝑐𝑜𝑛𝑑 𝑏𝑟𝑎𝑛𝑐ℎ 12%
+( ) ∙ [60% (2.0 𝐶𝑃𝐼𝐵𝑟𝑎𝑛𝑐ℎ ) + 40% (1.5 𝐶𝑃𝐼 𝐵𝑟𝑎𝑛𝑐ℎ )]
⏟𝑐𝑜𝑛𝑑 𝑚𝑜𝑣𝑒 1% 𝑇𝑎𝑘𝑒𝑛 𝑁𝑜𝑡 𝑇𝑎𝑘𝑒𝑛
∑ 13%

𝐶𝑃𝐼𝑒𝑓𝑒𝑐𝑡𝑖𝑣𝑜 ≅ 99%

Nota: No se llega al 100% de la estadística por los redondeos, por ejemplo, no es posible que el cálculo de
multiplicación (mul) sea exactamente 0%.

178
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P4. (Problema extraído de Arquitectura de Computadores, Un Enfoque Cuantitativo, Morgan Kaufmann 2012.
Hennesy Patterson, 5ª edición). Usando la combinación de instrucciones de la Figura A.28, calcular el CPI efectivo
para instrucciones de coma flotante para el procesador MIPS. Asumir las mismas medidas de CPI promedio para
este tipo de instrucciones del ejercicio anterior P3.

Figura A.28. Conjunto de instrucciones dinámicas del MIPS para cinco programas SPECfp2000. Tenga en cuenta
que las instrucciones de movimiento de registro a registro entero están incluidas en la instrucción OR. Las
entradas en blanco tienen el valor de 0.0%.

Solución:

Similar al ejercicio anterior pero teniendo en cuenta las operaciones de coma flotante en lugar de las enteras.

179
P5. Hallar la aceleración entre una máquina M1 y otra M2 si:

 M1 ejecuta el doble de instrucciones que M2


 M1 tiene la cuarta parte de CPI que M2
 La frecuencia de reloj de M2 es el doble que la de M1

Solución:

Comparativa de las características de las dos máquinas:

M1 M2
Nº Instrucciones 2∙N N
CPI CPI 4 ∙ CPI
Frecuencia f 2∙f

Sabiendo que el tiempo de procesado de una máquina cualquiera es:


𝑁º𝐼𝑛𝑠𝑡 ∙ 𝐶𝑃𝐼
𝑡 = 𝑁º𝐼𝑛𝑠𝑡 ∙ 𝐶𝑃𝐼 ∙ 𝑃𝑒𝑟𝑖𝑜𝑑𝑜 → 𝑡=
𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎

Para calcular la aceleración entre dos máquinas se establece mediante el cociente entre los tiempos de
la máquina que menor tiempo requiere para el procesado entre la que requiere más tiempo. Por tanto,
𝑁º𝐼𝑛𝑠𝑡 ∙ 𝐶𝑃𝐼 (2 ∙ 𝑁) ∙ (𝐶𝑃𝐼)
( ) ⁄(𝑓)
𝑡𝑀1 𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎 𝑀1
𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛 = = = = 1
𝑡𝑀2 𝑁º𝐼𝑛𝑠𝑡 ∙ 𝐶𝑃𝐼 (𝑁) ∙ (4 ∙ 𝐶𝑃𝐼)
( ) ⁄(2 ∙ 𝑓)
𝐹𝑟𝑒𝑐𝑢𝑒𝑛𝑐𝑖𝑎 𝑀2

» Por tanto, las máquinas M1 y M2 son iguales desde el punto de vista del rendimiento.

180
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P6. Suponga que un compilador consigue paralelizar, para N procesadores, la mitad de un determinado código.
Usando la Ley de Amdahl, calcule la aceleración total conseguida. Hallar el límite cuando N tiende a infinito.

Solución:

Sabemos, por la Ley de Amdal, que


𝑡𝑖𝑒𝑚𝑝𝑜𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 1
𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛𝑡𝑜𝑡𝑎𝑙 = =
𝑡𝑖𝑒𝑚𝑝𝑜𝑚𝑒𝑗𝑜𝑟𝑎𝑑𝑜 𝐹
(1 − 𝐹 ) +
𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛𝑝𝑎𝑟𝑐𝑖𝑎𝑙

Si repartimos el trabajo entre N procesadores, entonces Aceleraciónparcial = N

Por otro lado, sabemos que la mitad del código sigue igual y que la otra mitad se ha conseguido
paralelizar, luego
1
𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛𝑡𝑜𝑡𝑎𝑙 =
0′50
(1 − 0′50) +
𝑁
Determinamos el valor del límite cuando este tiende a infinito:

1 1 1
lim ( ) = lim ( ) = = 2
𝑁→∞ 0′50 𝑁→∞ 0′ 50𝑁 + 0′50 0′50
(1 − 0′50) +
𝑁 𝑁

» Por tanto, se logrará una aceleración del 100%

P7. Suponga que se desea conseguir una aceleración de 80 en un sistema con 100 procesadores. Usando la Ley
de Amdahl, calcule la fracción del código que debe ser paralela.

Solución:

Sabiendo que la aceleración es 80 y el número de procesadores son 100, entonces:


1
80 = ⇒ 𝐹 = 0′997
𝐹
(1 − 𝐹 ) +
100

» Por tanto, el 99’7% del código deberá ser paralelizado.

181
P8. Dado el siguiente código, y suponiendo que en la parte optimizada (desenrollada) se consigue una
aceleración de 10, calcule la aceleración total conseguida en cada caso.

#define VECTOR_SIZE (1024*1024)

double a = 3, x[VECTOR_SIZE * 10], y[VECTOR_SIZE * 10];


// Asuma que los vectores están inicializados

Solución:

int Daxpy_no_opt(void) {
int i;
for (i = 0; i < VECTOR_SIZE; i++) Caso base sin optimizar:
x[i] = a * x[i] + y[i];
for (i = VECTOR_SIZE; i < VECTOR_SIZE * 10; i++)
x[i] = a * x[i] + y[i]; 𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛𝑡𝑜𝑡𝑎𝑙 = 1
return i;
}

int Daxpy_opt_10(void) {
int i;
for (i = 0; i < VECTOR_SIZE; i += 4) {
x[i] = a * x[i] + y[i]; Caso: Se optimiza 10 veces el 10% del código:
x[i + 1] = a * x[i + 1] + y[i + 1];
x[i + 2] = a * x[i + 2] + y[i + 2];
1
x[i + 3] = a * x[i + 3] + y[i + 3]; 𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛𝑡𝑜𝑡𝑎𝑙 = = 1′099
} 0′10
+ 0′90
for (i = VECTOR_SIZE; i < VECTOR_SIZE * 10; i++) 10
x[i] = a * x[i] + y[i];
return i;
}

int Daxpy_opt_90(void) {
int i;
for (i = 0; i < VECTOR_SIZE; i++)
x[i] = a * x[i] + y[i]; Caso: Se optimiza 10 veces el 90% del código:
for (i = VECTOR_SIZE; i < VECTOR_SIZE * 10; i += 4) {
x[i] = a * x[i] + y[i]; 1
x[i + 1] = a * x[i + 1] + y[i + 1]; 𝐴𝑐𝑒𝑙𝑒𝑟𝑎𝑐𝑖ó𝑛𝑡𝑜𝑡𝑎𝑙 = = 5′263
0′90
x[i + 2] = a * x[i + 2] + y[i + 2]; 0′ 10 + 10
x[i + 3] = a * x[i + 3] + y[i + 3];
}
return i;
}

Conclusión:

» Se ha obtenido una mejor aceleración del proceso al optimizar la parte más repetitiva.

182
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P9. (Problema extraído de Arquitectura de Computadores, Un Enfoque Cuantitativo, Morgan Kaufmann 2012.
Hennesy Patterson, 5ª edición) En este ejercicio, suponemos que estamos considerando la mejora de una
máquina mediante la adición de hardware vectorial. Cuando se ejecuta un cálculo en modo vectorial en el
hardware, es 10 veces más rápido que en el modo normal de ejecución. Lo llamamos el porcentaje de tiempo
que podría ser empleado en el modo vector el porcentaje de vectorización. Los vectores se discuten más
adelante, pero no es necesario saber nada acerca de cómo trabajan para responder a esta pregunta.

a) Dibuje un gráfico que represente la aceleración como un porcentaje del cálculo realizado en el modo
vectorial. Rotule el eje y "Aceleración Net" y la etiqueta del eje x "Vectorización porcentual".

b) ¿Qué porcentaje de vectorización se necesita para lograr una aceleración de 2?

c) ¿Qué porcentaje del tiempo de ejecución de cálculo se gasta en modo vectorial si se logra una
aceleración de 2?

d) ¿Qué porcentaje de vectorización se necesita para alcanzar la mitad de la aceleración máxima posible
utilizando el modo de vector?

e) Supongamos que usted ha medido el porcentaje de vectorización del programa para ser del 70%. El
grupo de diseño de hardware estima que puede acelerar el hardware vectorial aún más con una inversión
adicional significativa. Uno se pregunta si el personal para el compilador podría aumentar el porcentaje
de vectorización en su lugar. ¿Qué porcentaje de vectorización necesitaría el equipo para el compilador
para lograr igualar una adición 2 × aceleración en el vector unitario (más allá del inicial de 10×)?

183
184
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Test 0.

¿Has asistido hoy a la sesión de prácticas?

 SÍ.
 NO.

¿Qué crece más cada año para memorias RAM?

 No crecen prácticamente.
 El ancho de banda.
 La latencia de acceso.
 Ambos igual aproximadamente.
 La inversa de la latencia de acceso.

¿Qué significa que el CPI es menor que 1 para una CPU? (varias respuestas pueden ser correctas)

□ Es imposible, por la definición matemática de CPI.


□ Que la CPU tarda más de un ciclo para ejecutar una instrucción.
□ Que el tiempo de ejecución de un programa puede ser mayor que el número de ciclos invertidos
multiplicado por el periodo de la CPU.
 Que la CPU es capaz de ejecutar más de una instrucción por ciclo.

185
186
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 1 – Prestaciones en filtros de imágenes.

1. Objetivos y preparación.

En esta práctica vamos a demostrar cómo se puede mejorar el rendimiento de un programa, bien ayudando al
compilador, o bien escribiendo el código de alto nivel de la forma más adecuada para que el computador
aproveche mejor el paralelismo.

Para ello, se usará una forma sencilla de medir tiempos de ejecución y se calcularán aceleraciones para varias
versiones de un mismo programa, usando el IDE VS2010 (Visual Studio 2010 de Microsoft).

En concreto, se va a estudiar la optimización de filtros de tratamiento de imágenes, atacando a la parte del código
donde está la mayor parte del tiempo de ejecución (bucle central de bucles anidados). El filtro que se probará
realiza un difuminado de la imagen, que sirve para eliminar “ruido” (es decir, suprimir o reducir los píxeles con
valores muy diferentes de sus píxeles vecinos) y para otras aplicaciones.

Se compararán los tiempos de ejecución del filtro de la librería OpenCV con 2 versiones escritas en lenguaje C++.

Antes de acudir al laboratorio el alumno deberá:

- Repasar los conceptos básicos del tema 1. Los que se van a pedir expresamente en esta práctica son:
aceleración, fórmula del tiempo de ejecución y CPI.

- Entender cómo están organizados los esquemas de código que se proporcionan con este enunciado. Ver
Apéndice: Esqueleto del código de prueba.
Apéndice: Función simple para filtro de imágenes.

- Notar que img → height es la altura en píxeles de la imagen y que img → width es la anchura en píxeles
de la imagen.

- Dibujar en un esquema los píxeles de una imagen RGB (un Byte por color; 3 Bytes por píxel: Red, Green,
Blue) y del algoritmo que hace la función asd_blurring_simplest(). Para ello se debe saber que los datos
de una imagen RGB se organizan como un vector, cuyos elementos contienen 3 Bytes, de esta forma
(siendo un Byte cada celda de este diagrama, y Nf: número de filas, Nc = número de columnas):

187
2. Realización de la práctica.

El profesor explicará:

- Cómo medir el tiempo usando la clase QPTimer que se da (Ver Apéndice: La clase QPTimer).
- Estos comentarios del filtro:
// Remark: this loop has been chosen to avoid boundary conditions be checked.
// in fact two rows and two columns are not processed (this is a little difference: less than 0.3%)
- Cómo cambiar las opciones de optimización de la compilación.

En el aula se dará un proyecto de VS2010 con todas las configuraciones ya preparadas (entre ellas, una serie de
rutas configuradas).

NOTA: Si se quiere trabajar en otro PC (por ejemplo en casa) o se quiere empezar un proyecto nuevo de VS2010,
hay que realizar una serie de pasos de configuración (ver Configuración básica de Windows y Visual
Studio 2010 para OpenCV al final del documento).

Descargar el fichero comprimido de ev.us.es. Descomprimirlo sobre una carpeta de C: (Nunca trabajar sobre el
comprimido).

A continuación abrir la “solución” ASD_P1.sln” haciendo doble clic (una solución es un conjunto de proyectos de
VS2010). Si VS2010 abriera una ventana para la elección de configuración del lenguaje, entonces seleccionar
“configuración de desarrollo de Visual C++” y luego “Iniciar Visual Studio” (tardará más de un minuto en
configurar herramientas de C++), según se ve en la siguiente figura:

Vamos a comparar la duración de dos rutinas que hacen lo mismo con el vídeo, pero escritas de forma diferente:

- cvSmooth(), que pertenece a OpenCV.


- asd_blurring_simplest().

188
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2.1. Código más “simple”.

Empecemos comparando la rutina de la librería OpenCV cvSmooth(), con la “normal” o simple que cualquier
programador de alto nivel escribiría, es decir, asd_blurring_simplest().

Ver el código C++ para asegurarse de que los dos cronómetros (crono_my_own_blurring y crono_opencv_lib)
están midiendo estas rutinas.

Ejecutar y anotar los tiempos de ambas para estas dos opciones de optimización de la compilación: /Od, /O2.

Recordar que para cambiar estas opciones de compilación, lo más fácil es hacer clic con el botón derecho del
ratón sobre el nombre del Proyecto (en el explorador de las soluciones), y en el árbol de navegación de la
siguiente figura, elegir primero: /Od, es decir, toda optimización está deshabilitada. Generar la solución y ejecutar
sin iniciar la depuración (Ctrl – F5)

Después, para la otra medición, elegir /O2, para una mayor velocidad de ejecución. Volver a generar la solución
y ejecutar sin iniciar la depuración (Ctrl – F5)

A continuación anotar tanto la aceleración como los frames por segundo (fps) que se podrían procesar entre
ambas rutinas para ambas opciones.

NOTA: La ejecución será más rápida, si no se define esta constante (se ha preparado el código con compilación
condicional, para que así no se muestren los vídeos). Para ello, comentar la línea: #define __VIDEO_ON

¿Por qué sale igual tiempo de cvSmooth() tanto para /Od como para /O2?
¿Cuál de los tiempos es más correcto para estas mediciones: el mínimo o la media (Minimum time o Mean time)?
¿Por qué?

189
2.2. Cálculo del CPI.

A continuación el profesor explicará:

- Cómo despejar CPI de fórmula del tiempo de ejecución.


- Cómo arrancar el debugger, pararse en el bucle central y ver el desensamblado (ver figura siguiente).

Aquí vamos a trabajar con asd_blurring_simplest (), y calcular su CPI.

Hallar el número de instrucciones de la rutina, se arranca el depurador y se cuentan las instrucciones del bucle
central del ensamblador.

NOTA: Se puede copiar el texto de la ventana desensamblado y pegarlo en un editor para contar las
líneas, donde cada línea es una instrucción.

Finalmente se multiplican por el número de iteraciones de los otros bucles externos.

Hallar también el CPI.

Hacer lo anterior para la opción /Od y la opción /O2.

Contestar a:

- ¿Qué error se comete aproximadamente al medir sólo las instrucciones del bucle central del
ensamblador, cuando se usa /O2?

- Atendiendo a los tres parámetros Tiempo, Nº Instrucciones, CPI, ¿de dónde viene la principal diferencia
del tiempo de ejecución entre la ejecución con la opción /Od y con la opción /O2?

190
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Apéndice: Esqueleto del código de prueba.

// project def and includes



//-----------------------------------------------------
// My own blurring function
void asd_blurring_simplest(IplImage* img, int kernel_radius);

int main(int argc, char** argv) {
char *file_name = "megamind.avi";
// video to be processed
// OpenCV function to create windows:
#ifdef __VIDEO_ON
cvNamedWindow( "ASD_P1-in", CV_WINDOW_AUTOSIZE );
cvNamedWindow( "ASD_P1-out-OPENCV", CV_WINDOW_AUTOSIZE );
cvNamedWindow( "ASD_P1-out-MY-OWN-BLURRING", CV_WINDOW_AUTOSIZE );
#endif

// structures to point to a frame of a video, and capturing the first frame
IplImage* newframe;
IplImage* oldframe;
newframe = cvQueryFrame(g_capture);

// main loop where frames are processed
while (newframe… ) {
oldframe = newframe;
// code to start the timer is introduced here to measure filter times
// Do the smoothing using OpenCV
cvSmooth(oldframe, out, CV_BLUR, 3, 3);
// code to stop the timer is introduced here to measure filter times
// code to start the timer is introduced here to measure filter times

// UNCOMMENT THIS FUNCTION (and coment the other) WHEN NECESSARY :
asd_blurring_simplest(oldframe, 1); //this function modify image (parameter)
// code to stop the timer is introduced here to measure filter times

// code to Show the original and the smoothed image in output windows is by here
// a new frame is captured to iterate the loop
newframe = cvQueryFrame(g_capture);
} // end of: while(newframe … ) {
// print the times of the different filters
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////
// My own blurring function (the "simplest")
void asd_blurring_simplest(IplImage* img, int kernel_radius) {

// see Appendix 4. APÉNDICE: FUNCIÓN SIMPLE PARA FILTRO DE IMÁGENES
}

191
Apéndice: Función simple para filtro de imágenes.

///////////////////////////////////////////////////////////////////
// My own blurring function (the "simplest")
void asd_blurring_simplest(IplImage* img, int kernel_radius) {
int number_of_neighbours = (1 + 2 * kernel_radius) * (1 + 2 * kernel_radius);
// Remark: this loop has been chosen to avoid boundary conditions be checked.
// in fact 2 rows and 2 columns are not processed (a little difference: less than 0.3%)
for (int y = 1; y < (img->height) - 1; y++) {
// THIS POINTER ptr (to unsigned char) will point to the first byte of a row in a RGB image
// Do not worry about the fields of this structure; they are given by OpenCV
uchar* ptr = (uchar*) (img->imageData + y * img->widthStep);
int image_width = img->width;
// Remark: this loop has been chosen to avoid boundary conditions be checked.
// in fact 2 rows and 2 columns are not processed (a little difference: less than 0.3%)
for (int x = 1; x < (image_width - 1); x++) {
for (int color = 0; color < 3; color++) {
int sum = 0;
for (int yshift = -kernel_radius; yshift <= kernel_radius; yshift++) {
for (int xshift = -kernel_radius; xshift <= kernel_radius; xshift++) {
sum += ptr[3 * (x + xshift) + 3 * image_width * yshift + color];
}
}
ptr[3 * x + color] = sum / number_of_neighbours;
}
}
}
}

Apéndice: La clase QPTimer.

QPTimer crono; //creates an object QPTimer


crono.Calibrate(); //calibrates timer overhead and set cronometer to zero
// repeat several times the test for more timing precision.
for ( int times=0; times < 10; times++)
{
crono.Start(); // start timer
// Do Something
function_to_be_measured ();
crono.Stop(); // stop timer and increment the number of measures
crono.Reset(); // set timer to zero
}
// print timing results (for the previous the number
// of measures; crono.ResetAll() would have reset this number)
crono.PrintMinimumTime (" Minimum time in seconds is: ");
crono.PrintMeanTime (" Mean time in seconds is : ");
crono.PrintMinimumCycles (" Minimum time in cycles is : ");
crono.PrintMeanCycles (" Mean time in cycles is : ");

192
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

TABLA DE RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 1. PRESTACIONES DE FILTROS DE IMÁGENES.

ALUMNO:

Objetivos y Preparación.

Dibujar un diagrama de cómo se organizan los datos de una imagen RGB.

Requiere de 3 Bytes, uno por color (Red, Green, Blue).


En la gráfica, un Byte indicado como 1 significa valor
255, mientras que los indicados como 0 significan 0.
Estos últimos podrán tomar valores de 0 a 255
obteniendo valores cromáticos intermedios.

Dibujar en un esquema de los píxeles de una imagen RGB (3 Bytes por píxel: Red, Green, Blue) y del algoritmo
que hace la función simplest ().

La imagen se almacena en forma de vector, esto es, por filas.


Almacena en cada posición 3 Bytes, uno por cada componente
del color (RGB).

La función simplest() obtiene la media aritmética, de cada


componente de color, de un conjunto de píxeles aledaños con
un marco de radio 3 a cada pixel de la imagen y establece así el
nuevo color de este.

193
2.1 Código más “simple”
CvSmooth asd_blurring_simplest
Tiempo con la opción /Od 0’00510262 0’07736
Tiempo con la opción /O2 0’00511623 0’0340358
0'07736
Aceleración entre ambos con /Od 1.0 = 15'1608
0'00510262

0'0340358 0’07736 s → 1 frame


Aceleración entre ambos con /O2 1.0 = 6'6525 1 s → x
0'00511623

1 1
Frames por segundo (fps) = 195'977 = 12'926
0'00510262 0'07736

Contestar ¿Por qué sale igual tiempo de cvSmooth() tanto para /Od como para /O2?

Porque la librería cvSmooth() ya está compilada y únicamente se linka de forma directa al programa,
por tanto no se le aplica ninguna aceleración.

¿Cuál de los tiempos es más correcto para estas mediciones: el mínimo o la media (Minimum time o Mean
time)? ¿Por qué?

La media sería que el sistema operativo tiene cambio de contexto, mientras que sería mejor tomar el
valor mínimo, ya que es un valor realmente alcanzable por la máquina.

2.2 Cálculo del CPI


asd_blurring_simplest asd_blurring_simplest
con /Od con /O2
Nº Instrucciones por
22 - 4 (overhead – hasta Jump) = 19
iteración en el bucle 19 53
más interno (.asm).
Nº Instrucciones por 19 ∙ xShift ∙ yShift ∙ Color
19 x 3 x 3 x 3 = 513 53 x 9 = 477
píxel.
Nº Instrucciones por
19 ∙ xShift ∙ yShift ∙ Color ∙ (width-2) ∙ (height-2)
iteración aproxim. de 477 x 718 x 526 = 180.147.636
19 x 3 x 3 x 3 x 718 x 526 = 193.743.684
un frame.
Tejecución ∙Frecuencia 0'7736 ∙ 2'33∙10-9 0'0340358 ∙ 2'33∙10-9
CPI aproximado CPI = = = 0'93 CPI = = 0'44
Num. Instrucciones 193.743.684 180.147.636
¿Qué error se comete aproximadamente al medir sólo las instrucciones del bucle central del ensamblador
cuando se usa /O2?
4
Caso: /Od: Error = (19∙3) = 0'065 es decir 6’5% de error
+4
Caso: /O2:

Atendiendo a los tres parámetros Tiempo, N Instrucciones, CPI, ¿de dónde viene la principal diferencia del
tiempo de ejecución entre la ejecución con la opción /Od y con la /O2?

El CPI varía prácticamente el doble: de 0’065 (/Od) a 0’4413 (/O2)


N. Instrucciones varía un 10% aproximadamente (de 193.743.684 a 180.147.636)

194
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 2 – Aceleración de filtros de imágenes.

1. Objetivos y preparación.

En esta práctica vamos a continuar trabajando en la mejora del rendimiento de un programa, no sólo ayudando
al compilador, sino también escribiendo el código de alto nivel de la forma más adecuada para que el computador
aproveche mejor el paralelismo. En este caso vamos a trabajar con el desenrollado visto en teoría.

Para ello se va a continuar con el mismo proyecto IDE VS2010 de la práctica 1 de la asignatura.

Se compararán los tiempos de ejecución del filtro de la librería OpenCV con una versión desenrollada del código
simplest() escrita en lenguaje C++ (unrolled) que se ofrece y otra que el alumno realizará. Además se
compararán los tiempos y códigos en ensamblador que se obtienen con varias opciones de compilación.

Antes de acudir al laboratorio el alumno deberá:

- Terminar los apartados pendientes de la Práctica 1.


- Pensar un código de alto nivel que desenrolle los bucles más anidados (color, yshift o xshift) del código
simplest().
- Plantear si se podría desenrollar todo el procesado del kernel y los colores.

2. Realización de la práctica.

El profesor explicará el concepto de desenrollar un bucle.

En el aula se tomará el proyecto de VS2010 de la Práctica 2 con todas las configuraciones ya preparadas, y
disponible en WebCT (entre ellas, una serie de rutas configuradas).

NOTA: Si se quiere trabajar en otro PC (por ejemplo en casa) o se quiere empezar un proyecto nuevo de VS2010,
hay que realizar una serie de pasos de configuración (ver Configuración básica de Windows y Visual
Studio 2010 para OpenCV al final del documento).´

Vamos a comparar la duración de dos rutinas que hacen lo mismo con el vídeo, pero escritas de forma diferente:

- cvSmooth(), que pertenece a OpenCV.


- asd_blurring_simplest().

Hacer las medidas de tiempos pero comparando:

- cvSmooth(), que pertenece a OpenCV.


- asd_blurring_unrolled().

Contestar a las cuestiones de la tabla de resultados.

195
Vamos a trabajar con asd_blurring_unrolled() para crear un nuevo código en el que además, se desenrolle el
color. Tener en cuenta que los colores de las imágenes son independientes entre ellos para este filtro, por lo que
atienden a un desenrollado sistemático.

Una vez creado el nuevo código, medir las prestaciones para ambas opciones de compilación:

- Hallar el CPI con la opción /Od.


- Obtener el CPI con la opción /O2.
- Calcular también la Aceleración entre asd_blurring_unrolled() y el nuevo, así como entre cvSmooth() y
el nuevo.
- Contestar a la siguiente cuestión: Justifique las diferencias que observa en los resultados entre la versión
unrolled que se da y la versión que usted ha desenrollado para la opción de compilación /O2:

///////////////////////////////////////////////////////////////////
// My own blurring function (the "simplest")
void asd_blurring_unrolled ( IplImage* img ) {
// Remark: this loop has been chosen to avoid boundary conditions be checked.
// in fact two rows and two columns are not processed (this is a little difference:
// less than 0.3%)
for( int y = 1; y < (img->height)-1; y++ ) {
uchar* ptr = (uchar*) (img->imageData + y * img->widthStep);
int image_width = img->width;
// Remark: this loop has been chosen to avoid boundary conditions be checked.
// in fact two rows and two columns are not processed (this is a little difference:
// less than 0.3%)
for( int x = 1; x < (image_width -1); x++ ) {
for( int color = 0; color < 3; color++ ) {
int sum = 0;
sum += ptr[3 * x + color];
sum += ptr[3 * (x-1) + color];
sum += ptr[3 * (x+1) + color];

sum += ptr[3 * x + 3*image_width + color];


sum += ptr[3 * (x-1) + 3*image_width + color];
sum += ptr[3 * (x+1) + 3*image_width + color];

sum += ptr[3 * x - 3*image_width + color];


sum += ptr[3 * (x-1) - 3*image_width + color];
sum += ptr[3 * (x+1) - 3*image_width + color];
ptr[ 3*x + color] = sum / 9;
}
}
}
}

196
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

TABLA DE RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 2. ACELERACIÓN DE FILTROS DE IMÁGENES.

ALUMNO:

Objetivos y Preparación.

Versión desenrollada de alto nivel del bucle interno elegido.

void asd_blurring_unrolled_own(IplImage* img) {


for (int y = 1; y < (img->height) - 1; y++) {
uchar* ptr = (uchar*) (img->imageData + y * img->widthStep);
int image_width = img->width;
for (int x = 1; x < (image_width - 1); x++) {

int sum0 = 0;
int sum1 = 0;
int sum2 = 0;

sum0 += ptr[3 * x + 0];


sum1 += ptr[3 * x + 1];
sum2 += ptr[3 * x + 2];

sum0 += ptr[3 * (x - 1) + 0];


sum1 += ptr[3 * (x - 1) + 1];
sum2 += ptr[3 * (x - 1) + 2];

sum0 += ptr[3 * (x + 1) + 0];


sum1 += ptr[3 * (x + 1) + 1];
sum2 += ptr[3 * (x + 1) + 2];

sum0 += ptr[3 * x + 3 * image_width + 0];


sum1 += ptr[3 * x + 3 * image_width + 1];
sum2 += ptr[3 * x + 3 * image_width + 2];

sum0 += ptr[3 * (x - 1) + 3 * image_width + 0];


sum1 += ptr[3 * (x - 1) + 3 * image_width + 1];
sum2 += ptr[3 * (x - 1) + 3 * image_width + 2];

sum0 += ptr[3 * (x + 1) + 3 * image_width + 0];


sum1 += ptr[3 * (x + 1) + 3 * image_width + 1];
sum2 += ptr[3 * (x + 1) + 3 * image_width + 2];

sum0 += ptr[3 * x - 3 * image_width + 0];


sum1 += ptr[3 * x - 3 * image_width + 1];
sum2 += ptr[3 * x - 3 * image_width + 2];

sum0 += ptr[3 * (x - 1) - 3 * image_width + 0];


sum1 += ptr[3 * (x - 1) - 3 * image_width + 1];
sum2 += ptr[3 * (x - 1) - 3 * image_width + 2];

sum0 += ptr[3 * (x + 1) - 3 * image_width + 0];


sum1 += ptr[3 * (x + 1) - 3 * image_width + 1];
sum2 += ptr[3 * (x + 1) - 3 * image_width + 2];

ptr[3 * x + 0] = sum0 / 9;
ptr[3 * x + 1] = sum1 / 9;
ptr[3 * x + 2] = sum2 / 9;

}
}
}
197
2.1 Código desenrollado
CvSmooth asd_blurring_simplest
Tiempo con la opción /Od 0’0052230 0’0392525
Tiempo con la opción /O2 0’0052246 0’0051549
Aceleración entre CvSmooth 0'0392525
1.0 = 7'5153
y unrolled con opción /Od 0'0052230

Aceleración entre CvSmooth 0'0052246 Tiempoempleado s → 1 frame


1.0 = 1'0135 1 s → x
y unrolled con opción /O2 0'0051549

/Od: 191’4608 /Od: 25’4761


Frames por segundo (fps) /O2: 191’4022 /O2: 193’9902

Contestar ¿A qué bucles sustituyen estas líneas de código?

sum += ptr[3 * x + color];


sum += ptr[3 *(x-1) + color];
… …
sum += ptr[3 *(x+1) - 3*image_width + color];

» Las líneas de código anteriores sustituyen a los bucles xshift y yshift.

2.2 Mejorando las prestaciones


CvSmooth asd_blurring_unrolled Nuevo Código Alumno
Tiempo con la opción /Od 0’0052230 0’040512 0’0262113
Tiempo con la opción /O2 0’0052246 0’005075 0’0062729
Aceleración entre CvSmooth 0'0262113
1.0 X = 5'0184
y propio con opción /Od 0'0052230

Aceleración entre CvSmooth 0'0062729


1.0 X = 1'2006
y propio con opción /O2 0'0052246

Aceleración entre unrolled 0'0062729


X 1.0 = 1'2360
y propio con opción /Od 0'005075

Aceleración entre unrolled 0'040512


X 1.0 = 1'5456
y propio con opción /O2 0'0262113

/Od: 191’4608 /Od: 24’6840 /Od: 38’1515


Frames por segundo (fps) /O2: 191’4022 /O2: 197’0443 /O2: 159’4159

Justifique las diferencias que observa en los resultados entre la versión unrolled que se da y la versión que
usted ha desenrollado más iteraciones para la opción de compilación /O2:

» Cuando el compilador encuentra un bucle estándar éste aplica un conjunto de técnicas de


optimización capaz de mejorar enormemente la sucesión de instrucciones de desenrollado que
podamos implementar manualmente como programadores.

198
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 3a (VS) – Procesadores superescalares. Límites del ILP.

1. Objetivos.

En esta práctica los alumnos estudiarán los límites de ILP que se pueden conseguir con técnicas dinámicas en
procesadores de propósito general (GPP). Para ello medirán el tiempo de ejecución de un código en C++ en una
máquina GPP real. Se estudiarán diferentes variaciones, en alguna de las cuales prácticamente se alcanza el límite
del flujo de datos (data-flow limit), de manera que el tiempo medio por iteración de un bucle es debido a tal
límite. Los alumnos deberán analizar cada código para razonar cuál es su ruta crítica y entender el motivo de las
diferencias de rendimiento entre las diferentes variaciones.

2. Preparación.

 Se da un código en C que resuelve un producto múltiple con una variable acumuladora. Es un bucle simple
para ver el máximo IPC que se puede conseguir.

 Los cálculos se repetirán con vectores de distintos tamaños, para estudiar el impacto del acceso a memoria
en el rendimiento, según el tamaño de los mismos.

 Al ejecutar en un GPP los programas reales (que se pueden descargar de ev.us.es), evidentemente se van a
obtener diferentes resultados de ciclos según la máquina donde se pruebe.

Nota: los programas están preparados para Visual Studio; si se usara el código fuente con otro compilador,
no olvidar activar opciones similares a: “Optimización completa (/Ox), Favorecer código rápido (/Ot), Modelo
de Punto Flotante: Fast”.

 El código se ha escrito lo más simple posible para que pueda probarse en cualquier compilador.

 Concretamente, para facilitar la ejecución, se proporciona un proyecto (o “solución”) creado con Visual
Studio 2010.

 La instrucción de medida de ciclos rdtsc funciona en todos los procesadores Intel, pero no en ciertos AMD.

 Si se quisiera medir tiempos en segundos, habría que modificar el valor de la constante siguiente en el fichero
de encabezado (cabecera, header): QueryPerformanceTiming_rdtsc.h:
#define CPU_FREQ_HZ (3.20e+9)
//write here your CPU frequency

Si no se hiciera el tiempo en segundos se calcularía mal.

 Hay que ejecutar sin depuración (tecla Ctrl F5) para que la ventana de comando no se cierre, o bien ejecutar
en una consola. Debe usarse el modo Release (que está preparado con todas las optimizaciones). Ver figura:

199
Asegurarse de elegir el modelo Fast de coma flotante y las instrucciones del núcleo SIMD (se estudiarán en teoría
más adelante), como se muestra en la siguiente figura:

 Para hacer esta prueba, no use combinaciones de energía del estilo de “Administración de energía mínima”,
pues puede ocasionar problemas en la medición de tiempos (la frecuencia de la CPU es variable en este
modo). En Windows, haga clic en Inicio y en Ejecutar, escriba powercfg.cpl y haga clic en Aceptar. Seleccione
una combinación de energía distinta de Administración de energía mínima en el cuadro Combinaciones de
energía.

200
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

3. Realización de la práctica.

Ejecute todo el proyecto VS2010 (ver ev.us.es) en modo Release. El proyecto que se da no usa OpenCV y por
tanto, debería funcionar en cualquier PC Intel (no funciona correctamente en procesadores AMD).

Evidentemente la duración del código real dependerá de la máquina concreta y del estado de carga de la
memoria y CPU. Se aconseja no tener abiertas muchas aplicaciones y ejecutar varias veces para quedarse con el
mínimo tiempo.

Se deben recoger los siguientes datos previos:

- Máquina (sobre todo el procesador) donde se ejecuta el código real. Se puede consultar en Panel de
control → Sistema. También puede usar el programa CPU-Z (http://www.cpuid.com/softwares/cpu-z.html),
el cual indica muchas de las características del procesador.

- Compilador y Opciones de optimización usadas.

Resultados:

Volver a habilitar las optimizaciones según se indicó anteriormente y:

- Ejecutar el proyecto en modo release y anotar los CPI, y los ciclos por iteración de test1().

- Dibujar un esquema de tal ruta crítica.

- Tratar de razonar cuál es la operación u operaciones que están en la ruta crítica, o la circunstancia que
hace que el CPI sea mayor que 1/4, o que la duración de una iteración sea superior a:
Numero de instrucciones por iteración de ensamblador
grado superescalaridad (4 en la mayoría de las CPU superescalares de alto rendimiento)
- Dibujar un cronograma temporal simplificado de cómo se están ejecutando las operaciones. Considerar
que hay 2 UF de MULT (que permiten segmentación).

- Indicar la latencia de la unidad funcional correspondiente a esa operación u operaciones, o en su caso,


la circunstancia que conduce a una duración mayor.

- Analizar qué ocurre cuando crece el número de iteraciones. Tratar de razonar el motivo por el que la
pérdida de rendimiento no se produce de igual manera en las distintas funciones.

A continuación, escribir el producto múltiple con 2, 3, 4, variables acumuladoras rellenando lo que falta en
test2() test3() test4(). Repetir los pasos anteriores.

Algunos detalles sobre el código a tener en cuenta:

Para poder ver el código ASM o depurar más cómodamente compruebe que esta opción está activa: Propiedades
del proyecto → Propiedades de configuración → C/C++ → Optimización → Expansión de las funciones inline →
Solo __inline (/Ob1)" (existen opciones similares en otros compiladores). De esa forma, las funciones test1(),
test2() etc. no se "empotran " en el código del main(), sino que tienen su cuerpo independiente y se llaman
(CALL ) desde el main(). Si tal opción no está activa, el compilador podría expandir todo el código de una función
dentro del ASM del main() y luego reordenar como le parezca, con lo cual el ASM no es fácil de seguir ni entender

Cada función tiene una sentencia return (p ej : return a[N_ITER-1 ];). Dejar siempre esta sentencia y las
impresiones de las variables devueltas. Si no se usa la variable de salida, el compilador podría eliminar
totalmente la llamada a la función.
201
APÉNDICE: Código fuente para las pruebas
//----------------------------------------------------
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Dpto ATC. www.atc.us.es ASD subject 3º GTI
// Versión 1. Nov. 2012. Fernando Díaz del Rio
// Versión 2. Mar. 2014. Jesús Rodríguez Leal
//
// - Doing a test to reach ILP limits in superscalar processor
// - TEST TIMING OF simple routine
//-----------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <iostream>
using namespace std;
//------------------------------------------------------------------
// project def and includes
// this test uses a new version of QueryPerformanceTiming to measure CPU cycles using rdtsc asm instr.
#include "QueryPerformanceTiming_rdtsc.h"
#define N_REPETIC 64
// repeat several times each test to extract the minimum time and to have the caches filled
#define N_ITER (4*1024*1024)
float a[N_ITER];
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// vectors init
void vectors_init() {
int i;
for (i = 0; i < N_ITER; i++) {
a[i] = 1.0f; // a[i] must have near to 1 values to avoid overflow/underflow in tests
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// tests
float test1(int niter) {
int i;
float acc = 1.0;
for (i = 0; i < niter; i++) {
acc = acc * a[i];
}
return acc;
}
float test2(int niter) {
int i;
float acc1 = 1.0;
float acc2 = 1.0; // initialization of accumulator variables
for (i = 0; i < niter % 2; i++) // this is a prolog code to check if niter is not multiple of 2
{
acc1 = acc1 * a[i];
}
// Write here the same algorithm using 2 accumulators
}
float test3(int niter) {
int i;
float acc1 = 1.0;
float acc2 = 1.0;
float acc3 = 1.0;
for (i = 0; i < niter % 3; i++) // this is a prolog code to check if niter is not multiple of 3
{
acc1 = acc1 * a[i];
}
// Write here the same algorithm using 3 accumulators
}
float test4(int niter) {
int i;
float acc1 = 1.0;
float acc2 = 1.0;
float acc3 = 1.0;
float acc4 = 1.0;
for (i = 0; i < niter % 4; i++) // this is a prolog code to check if niter is not multiple of 4
{
acc1 = acc1 * a[i];
}
// Write here the same algorithm using 4 accumulators
}

202
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char** argv) {
float var1, var2, var3, var4;
int i, div, niter;
vectors_init();
for (div = N_ITER / 256; div >= 1; div /= 2) {
niter = N_ITER / div;
QPTimer c1, c2, c3, c4;
c1.Calibrate(); //calibrates timer overhead and set cronometer to zero
c2.Calibrate(); //calibrates timer overhead and set cronometer to zero
c3.Calibrate(); //calibrates timer overhead and set cronometer to zero
c4.Calibrate(); //calibrates timer overhead and set cronometer to zero
// measuring tests
for (i = 0; i < N_REPETIC; i++) {
c1.Start(); // start timer
var1 = test1(niter); // Do the test
c1.Stop(); // stop timer
c1.Reset();
c2.Start(); // start timer
var2 = test2(niter); // Do the test
c2.Stop(); // stop timer
c2.Reset();
c3.Start(); // start timer
var3 = test2(niter); // Do the test
c3.Stop(); // stop timer
c3.Reset();
c4.Start(); // start timer
var4 = test4(niter); // Do the test
c4.Stop(); // stop timer
c4.Reset();
}
// end of testing
cout << endl
<< "ONLY PRINTING OUTPUT VARIABLE TO PREVENT THAT THE COMPILER ELIMINATES FUNCTION CALLS: "
<< var1 << ", " << var2 << ", " << var3 << ", " << var4 << endl;
cout << endl << "-Number of iterations N_ITER: " << niter << endl;
cout << endl << "-Number of measures: " << c1.NumberOfMeasures()
<< endl;
c1.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test1() is:", niter);
c2.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test2() is:", niter);
c3.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test3() is:", niter);
c4.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test4() is:", niter);
cout << endl;
}
}

203
TABLA DE RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 3A. PROCESADORES SUPERESCALARES: LÍMITES DEL ILP.

ALUMNO:

Datos previos:

Procesador: Intel Core 2 Duo Tamaño de cachés: 2 x 32 KBytes


Frecuencia de reloj: 2’33 GHz Ancho Banda Bus RAM: -
Optimización del compilador usada: Optimización completa: /Ox, Modelo de Punto Flotante: Fast

Código C resuelto:

//----------------------------------------------------
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Dpto ATC. www.atc.us.es
// ASD subject 3º GTI
// Versión 1. Nov. 2012. Fernando Díaz del Rio
// Versión 2. Mar. 2014. Jesús Rodríguez Leal
//
//
// - Doing several test to reach ILP limits in superscalar processor
// - TEST TIMING OF simple routine
//-----------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#include <iostream>

using namespace std;


//------------------------------------------------------------------
// project def and includes

// this test uses a new version of QueryPerformanceTiming ,


// to measure CPU cycles more precisely using rdtsc asm instr.
#include "QueryPerformanceTiming_rdtsc.h"

#define N_REPETIC 64
// repeat several times each test to extract the minimum time and to have the caches filled

#define N_ITER (16*1024*1024)

float a[N_ITER];

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// vectors init
void vectors_init() {
int i;
for (i = 0; i < N_ITER; i++) {
// a[i] must have near to 1 values to avoid overflow/underflow in tests
a[i] = 1.0f;
}
}

204
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// tests

float test1(int niter) {


int i;
float acc = 1.0;
for (i = 0; i < niter; i++) {
acc = acc * a[i];
}
return acc;
}

float test2(int niter) {


int i;
float acc1 = 1.0;
float acc2 = 1.0; // initialization of accumulator variables
for (i = 0; i < niter % 2; i++) // this is a prolog code to check if niter is not multiple of 2
{
acc1 = acc1 * a[i];
}
for (; i < niter; i = i + 2) // this is a prolog code to check if niter is not multiple of 2
{
acc1 = acc1 * a[i];
acc2 = acc2 * a[i + 1];
}
return acc1 * acc2;
}

float test3(int niter) {


int i;
float acc1 = 1.0;
float acc2 = 1.0;
float acc3 = 1.0;
for (i = 0; i < niter % 3; i++) // this is a prolog code to check if niter is not multiple of 3
for (; i < niter; i = i + 3) // this is a prolog code to check if niter is not multiple of 2
{
acc1 = acc1 * a[i];
acc2 = acc2 * a[i + 1];
acc3 = acc3 * a[i + 2];
}
return acc1 * acc2 * acc3;
}

float test4(int niter) {


int i;
float acc1 = 1.0;
float acc2 = 1.0;
float acc3 = 1.0;
float acc4 = 1.0;
for (i = 0; i < niter % 4; i++) // this is a prolog code to check if niter is not multiple of 4
{
acc1 = acc1 * a[i];
}
for (; i < niter; i = i + 4) // this is a prolog code to check if niter is not multiple of 2
{
acc1 = acc1 * a[i];
acc2 = acc2 * a[i + 1];
acc3 = acc3 * a[i + 2];
acc4 = acc4 * a[i + 3];
}
return acc1 * acc2 * acc3 * acc4;
}

205
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char** argv) {

float var1, var2, var3, var4;


int i, div, niter;

vectors_init();

for (div = N_ITER / 256; div >= 1; div /= 2) {


niter = N_ITER / div;

QPTimer c1, c2, c3, c4;


c1.Calibrate(); //calibrates timer overhead and set cronometer to zero
c2.Calibrate(); //calibrates timer overhead and set cronometer to zero
c3.Calibrate(); //calibrates timer overhead and set cronometer to zero
c4.Calibrate(); //calibrates timer overhead and set cronometer to zero

// measuring tests
for (i = 0; i < N_REPETIC; i++) {
c1.Start(); // start timer
var1 = test1(niter); // Do the test
c1.Stop(); // stop timer
c1.Reset();

c2.Start(); // start timer


var2 = test2(niter); // Do the test
c2.Stop(); // stop timer
c2.Reset();

c3.Start(); // start timer


var3 = test3(niter); // Do the test
c3.Stop(); // stop timer
c3.Reset();

c4.Start(); // start timer


var4 = test4(niter); // Do the test
c4.Stop(); // stop timer
c4.Reset();
}
// end of testing

cout << endl


<< "ONLY PRINTING OUTPUT VARIABLE TO PREVENT THAT THE COMPILER ELIMINATES FUNCTION CALLS: "
<< var1 << ", " << var2 << ", " << var3 << ", " << var4 << endl;
cout << endl << "-Number of iterations N_ITER: " << niter << endl;
cout << endl << "-Number of measures: " << c1.NumberOfMeasures() << endl;

c1.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test1() is: ", niter);
c2.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test2() is: ", niter);
c3.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test3() is: ", niter);
c4.PrintMinimumCyclesByIteration(
" Minimum time in cycles for an iteration of test4() is: ", niter);

cout << endl;


}
}

206
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Interpretación gráfica de los resultados obtenidos por consola:


N_iter test1() test2() test3() test4()
256 4,18359 2,10547 1,44922 1,25781
512 4,04688 2,05078 1,39453 1,06641
1024 4,01953 2,03027 1,36035 1,03223
2048 4,00928 2,01318 1,34668 1,01514
4096 4,00586 2,00635 1,34155 1,00830
8192 4,00330 2,00549 1,33899 1,03906
16384 4,00159 2,00293 1,33643 1,18048
32768 4,00073 2,00101 1,33429 1,17813
65536 4,00041 2,00047 1,33408 1,17802
131072 4,00020 2,00026 1,33370 1,19298
262144 4,00047 2,00047 1,33421 1,20911
524288 4,00395 2,01054 1,34392 1,22194
1048576 4,10627 2,31220 1,92446 1,85404
2097152 4,15234 2,30763 1,95347 1,97393
4194304 4,14950 2,33202 2,00789 2,01754
8388608 4,18054 2,34118 2,03006 2,03427
16777216 4,18449 2,35237 2,05429 2,05429
Nota: los valores del test1() están anotados a la derecha.

test1() test2()

test3() test4()

207
RESULTADOS:
Tiempo mínimo en ciclos por Dibujar un esquema del flujo de datos. Latencia de la UF en la ruta
iteración (niter) y CPI. Señalar Operación(es) en la ruta crítica (en ciclos).
Anotar para diversos tamaños de crítica. Indicar la circunstancia que
vectores. conduce a una duración
ciclos mayor.
iteración del bucle Dibujar un cronograma
simplificado.
Ver anexo compilación.

Para 256 iteraciones se


emplean 4’18359 segundos
cada una de ellas.
Ttot = 256 ∙ 4’18359 = 1071 s
Cada iteración ha generado
Un total de instrucciones =
x i=0
5 previas acc LF acc
+ 7 ∙ (256/4)
test1() x i=0 i ++
+ 3 posteriores
456 instrucc. de bajo nivel acc 4 ciclos que son el
1071 producto que pertenece a
CPI256 = = 2'348 la multiplicación.
456

Valor promedio de tiempos:


4’06170 segundos
Valor promedio de n_iter:
1973775 iteraciones
1973775∙4'06170
CPI = = 2'321
5+(7∙(1973775/4)+3 )

256 acc1 acc2 LF LF


x x
Ciclos = 2’28912; CPI = 0’908 acc1 acc2
+
x x acc1+acc2
16777216
test2() acc1 acc2
Ciclos = 2’14462; CPI = 0’872 + En este caso, se trata de 2
acc1+acc2 ciclos, tiempo requerido
Valor promedio de tiempos: i=0 para realizar 2 iteraciones,
2’11015 segundos en paralelo, la
multiplicación.
256 acc1 acc2 acc3 LF LF LF
Ciclos = 1’53177; CPI = 0’731
x x x Mismo razonamiento que
16777216 acc2
test3() acc1 acc3 el caso anterior, pero con
Ciclos = 1’43203; CPI = 0’719 + un tiempo de 32 ciclos.
acc1+acc2+acc3
Valor promedio de tiempos: i=0
1’54224 segundos
acc3 LF acc1 LF acc2 LF acc4 LF
Ciclos 256 = 1’1723
Ciclos 16777216 = 1’19344 x x x x
acc3 acc1 acc2 acc4
Mismo razonamiento que
test4()
+ el caso anterior.
Valor promedio de tiempos:
acc1+acc2+acc3+acc4
1’38316 segundos
i=0
208
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Bucles largos con vectores grandes.

Explicar el comportamiento del código cuando el tamaño del vector es elevado (anotar a partir de que N_ITER
hay cambios en los tiempos):

Desde la iteración 256 hasta 131.072 los tiempos disminuyen progresivamente desde 4’18359
hasta 4’0002. A partir de la iteración 262.144 los tiempos vuelven a aumentar, desde 4’00047 hasta
test1() 4’18449.
Denota una pendiente muy pronunciada en descenso de tiempos hasta la iteración 1024.
Denota una pendiente muy pronunciada en ascenso de tiempos a partir de la iteración 524.228.
Desde la iteración 256 hasta 131.072 los tiempos disminuyen progresivamente desde 2’10547
hasta 2’00026. A partir de la iteración 262.144 los tiempos vuelven a aumentar, desde 2’00047
test2() hasta 2’35237.
Denota una pendiente algo pronunciada en descenso de tiempos hasta la iteración 4.096.
Denota una pendiente muy pronunciada en ascenso de tiempos a partir de la iteración 524.228.
Desde la iteración 256 hasta 131.072 los tiempos disminuyen progresivamente desde 1’44922
hasta 1’3337. A partir de la iteración 262.144 los tiempos vuelven a aumentar, desde 1’33421 hasta
test3() 2’05429.
Denota una pendiente muy suave en descenso de tiempos hasta la iteración 4.096.
Denota una pendiente muy pronunciada en ascenso de tiempos a partir de la iteración 524.228.
Desde la iteración 256 hasta 8.192 los tiempos disminuyen progresivamente desde 1’25781 hasta
1’03223. A partir de la iteración 16.384 los tiempos vuelven a aumentar y se mantienen hasta la
test4() iteración 524.288, desde 1’18048 hasta 1’22194, que vuelve a subir con una pendiente muy
elevada hasta la iteración 1.048576 con un tiempo de 1’85404. Volviendo a suavizar nuevamente
la pendiente creada.

Anexo: Vista compilación de text1()


//////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// tests
float test1(int niter)
{
int i;
float acc = 1.0;
00CC14F0 movss xmm0,dword ptr ds:[00CC32ECh]
for (i = 0; i < niter; i++)
00CC14F8 xor eax,eax
00CC14FA cmp ecx,4 5 instrucciones previas
00CC14FD jl 00CC152D
00CC14FF lea edx,[ecx-3]
{
acc = acc * a[i];
00CC1502 mulss xmm0,dword ptr [eax*4+00CC4378h]
00CC150B mulss xmm0,dword ptr [eax*4+00CC437Ch]
Bucle desenrrollado: 4 instrucciones
00CC1514 mulss xmm0,dword ptr [eax*4+00CC4380h]
00CC151D mulss xmm0,dword ptr [eax*4+00CC4384h]
00CC1526 add eax,4
00CC1529 cmp eax,edx 3 instrucciones de control del bucle
00CC152B jl 00CC1502
for (i = 0; i < niter; i++)
00CC152D cmp eax,ecx
00CC152F jge 00CC153F
{
acc = acc * a[i];
00CC1531 mulss xmm0,dword ptr [eax*4+00CC4378h]
00CC153A inc eax
00CC153B cmp eax,ecx
00CC153D jl 00CC1531
}
return acc;
}

209
210
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 3a (DIG) – Planificación dinámica y Procesadores superescalares. Bucles vectoriales.

1. Objetivos.

En esta práctica se trata de estudiar el Algoritmo de Tomasulo y algunos ejemplos simples de bucles vectorizables
para distintos grados de superescalaridad m.

Se manejará un simulador visual desarrollado como proyecto Fin de carrera en el Departamento de Arquitectura
y Tecnología de Computadores de la Universidad de Sevilla: SUPERTOMASIM (SUPERescalarTOMasuloSIMulator).
Éste simula un procesador didáctico superescalar y superencadenado de un modelo de RISC típico de 32 bits
(llamado DLX), que dispone de “scheduling” dinámico (Algoritmo de Tomasulo). Además son configurables tanto
el grado de superescalaridad como los recursos del procesador: F.U. (Functional Units), CDB (Common Data Bus),
RS (Reservation Station), etc.

2. Preparación.

Fichero de código para preparar. Trabajaremos primero con un bucle simple:


; Cabecera con definición de variables e inicialización de los registros
.data ; las siguientes etiquetas sirven para obtener la dirección de comienzo de vectores o variables:
arrayx: .float 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25
arrayy: .float 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27
a : .float 3 ; Constante a. Además sirve de dirección donde acaba el vector y.
.text
addi r1, r0, arrayx ; Puntero a arrayx
addi r2, r0, arrayy ; Puntero a arrayy
addi r3, r0, a ; Puntero a variable a
lf f11, 0(r3) ; carga el valor de a en f11
; Bucle
bucle: lf f5, 0(r1) ; carga elemento del vector x
lf f6, 0(r2) ; carga elemento del vector y
multf f5, f11, f5
addf f5, f6 , f5
sf 0(r2), f5
addi r1, r1, 4
addi r2, r2, 4
slti r3, r2, a
bnez r3, bucle

Piense y resuelva las siguientes preguntas sobre el código dado. Anótelo en la tabla de resultados:

a) Dibuje las dependencias reales dentro de una iteración.

b) Tradúzcalo a lenguaje C o similar.

c) ¿Se puede desenrollar? ¿Se pueden computar en paralelo todos los elementos de los vectores x, y (es
decir, es vectorizable)? Razonar.

211
3. Realización de la práctica.

Descripción del simulador SUPERTOMASIM.

Este simulador admite cualquier código RISC (del llamado procesador DLX). Como se ve en la siguiente figura,
muestra el cronograma que se está ejecutando y el estado completo (RS, registros, memoria, etc.) del procesador
simulado.

Las etiquetas (nombre del registro interno tras el renombrado) se llaman igual que las RS (LOAD01, LOAD02, … ,
ALUINT01, ALUINT02, etc.). Los registros y RS que esperan por una etiqueta (por existir una dependencia real RAW)
muestran tal nombre en lugar del valor (por ejemplo, en los registros R1, F11 por las RS ALUINT01, MULFP02 de la
figura).

Y permite configurar en el menú Windows → Configuration (ver cuadro de diálogo inferior derecho de la imagen):

- Duración de cada F.U. (Latency).


- Número de RS (estaciones de reserva) (#RS).
- Número de F.U. (#F.U.).
- m = Grado de superescalaridad (Issue Width).
- Grado de superencadenamiento de la fase IS (IS Stages).
- Número de CDB (# of CDB).
- Fase en la que se realiza la predicción de la BTB (BTB Prediction Stage).

El cronograma y configuración se pueden salvar en ficheros aparte (el cronograma tiene cada fase separada por
tabuladores para que se pueda copiar fácilmente por ejemplo en una hoja de cálculo de Excel).

212
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejecutar el Simulador SUPERTOMASIM.

El simulador es la primera parte de un proyecto de simulación de procesadores superescalares, y algunos detalles


de la interfaz de usuario están aún incompletos, son poco amigables o no detectan ciertos errores de entrada.
Por tanto, se van a dar los ficheros de ensamblador en el laboratorio para evitar que salgan ciertos errores.

Consideraciones sobre la arquitectura que simula SUPERTOMASIM.

Cada bloqueo estructural se muestra repitiendo la fase que no puede avanzar. Por ejemplo: IF IF IF IS significa
que han habido dos ciclos de bloqueo porque IS no pudo ejecutarse (según notación de clase: IF - - IS).

La BTB predice saltos como siempre tomados (se podría cambiar esto usando un fichero llamado BTB.txt).

Para cambiar cualquier parámetro de la configuración se tendrá que realizar (cuadro de diálogo de configuración:
Windows → Configuration) o para cambiar de código simulado, debe primero resetear el simulador con la
opción: File → Reset Sim. Se pierde evidentemente todo el cronograma anterior.

1. Para bucle vectorizable (RISC ESCALAR).

La configuración que vamos a usar es la siguiente. Utilice


Windows → Configuration y luego LOAD. Cargue el fichero
asdtomasim1.xml (si estuviera deshabilitado la carga, resetear
el simulador con la opción: File → Reset Sim.). Para esta
prueba 1, se han puesto muchos CDBs y muchas F.U. para que
no haya esperas por estos problemas estructurales. Además,
hay bastantes R.S. excepto de Store (sólo hay 1 de Store, con
objeto de que aparezcan bloqueos totales más adelante).

Ahora cargue el código asdtomasim1.s.

Se pide:

d) Ejecute paso a paso (tecla F7) o de 5 en 5 ciclos (tecla F8) hasta que la primera Store se haya emitido
(haya hecho la fase IS). Si es necesario, mueva la barra de scroll del cronograma para poder ver los
últimos ciclos. Dibuje el contenido de las R.S. (que están ocupadas) de ADDF, MULF, SF justo cuando la
primera Store se haya emitido (haya hecho la fase IS). Explique por qué las R.S. tienen este contenido.

e) Ejecute la primera iteración paso a paso (tecla F7) o de 5 en 5 ciclos (tecla F8), o de 20 en 20 icono .
Mueva scroll del cronograma para poder ver los últimos ciclos. Copie el cronograma de la primera
iteración y compruebe que las dependencias reales están retrasando las fases EX (dibuje con flechas de
WB a EX los bypasses a través del CDB).

f) ¿Cuál es la cadena crítica de ejecución (cadena de RAWs más larga) en una única iteración?

g) Ejecute más iteraciones hasta que se compruebe que se ha llegado a un “estacionario” del bucle, es
decir, que todas las iteraciones tienen el mismo cronograma. ¿Cuánto vale el CPI real del bucle en el
estacionario? Nota: para medir ciclos reales, cuente desde la fase IS del primer LF de una iteración, hasta
la fase IS del primer LF de la siguiente iteración.

h) Si sólo hay una R.S. de Store, ¿por qué no se bloquea el procesador?

213
2. Para bucle vectorizable (RISC SUPERESCALAR).

Resetear el simulador con la opción: File → Reset Sim.

Modifique la configuración para que sea superescalar de grado 3 (Issue Width = 3).

Cargue el código asdtomasim1.s. Ejecute al menos 4 iteraciones.

Se pide:

i) Hallar el CPIideal, real y CPIbloqueo para las iteraciones 3 y 4. Recuerde: para medir ciclos reales, cuente
desde la fase IS del primer LF de iteración 3, hasta la fase IS del primer LF de iteración 5. El CPI bloqueo
puede calcularlo restando CPIreal - CPIideal o contando los bloqueos (esto puede resultar más difícil).

j) Resetear el simulador con la opción: File → Reset Sim y repita lo mismo para superescalar de grado 6
(Issue Width = 6). Hallar CPIideal, CPIreal y CPIbloqueo para las iteraciones 3 y 4.

k) ¿Por qué el CPIreal es el mismo para altos grados de superescalaridad (Issue Width)?

l) Resetear el simulador con la opción: File → Reset Sim, y aumente el número de R.S. de Store (y tal vez
de otras F.U.) hasta que no haya bloqueos por agotamiento de R.S. ¿Cree que el número máximo de R.S.
de Store consumidos u ocupados (sin que haya bloqueos) es proporcional al grado de superescalaridad
(Issue Width)? ¿Por qué?

APÉNDICE: SIMULADOR SUPERTOMASIM

Otros pormenores del simulador SUPERTOMASIM:

- El número máximo de RS de cada tipo es 150. El número máximo de F.U. de cada tipo es 10. Podría
interrumpirse la simulación si se superan estos límites (no se chequean estos rangos en la configuración).

- La configuración se puede salvaren un fichero (con la opción GUARDAR/SAVE) y luego recuperarla


(opción CARGAR/LOAD).

Cuando escriba código DLX, debe tener en cuenta los siguientes pormenores:

- No puede haber una línea en blanco al final del fichero.

- No distingue entre mayúsculas y minúsculas.

- Detrás de la directiva .text (cuando se escriba el código), no se pueden poner líneas en blanco, ni líneas
que comiencen por “;” (es decir, comentarios que ocupen toda la línea).

- Las etiquetas de salto deben empezar en el primer carácter de la línea y no pueden estar solas en una
línea, sino que detrás del “:” debe ir una instrucción válida; por ejemplo, así:
bucle: lbu r3, 0(r1) ; Carga Arrayx[i]

- Los registros flotantes de doble precisión se llaman d0, d1, d2, etc. (no son válidos los f0, f2, etc.).
214
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

TABLA DE RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 3A. (DIG) – PLANIFICACIÓN DINÁMICA Y PROCESADORES SUPERESCALARES: BUCLES VECTORIALES.

ALUMNO:

Preparación:

01 ; Cabecera con definición de variables e inicialización de los registros


02 .data ; las siguientes etiquetas sirven para obtener la dirección de comienzo de
vectores o variables:
03 arrayx: .float 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25
04 arrayy: .float 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27
05 a : .float 3 ; Constante a. Además sirve de dirección donde acaba el vector y.
06 .text
07 addi r1, r0, arrayx ; Puntero a arrayx
08 addi r2, r0, arrayy ; Puntero a arrayy
a) 09 addi r3, r0, a ; Puntero a variable a
10 lf f11, 0(r3) ; carga el valor de a en f11
11 ; Bucle
12 bucle: lf f5, 0(r1) ; carga elemento del vector x
13 lf f6, 0(r2) ; carga elemento del vector y
14 multf f5, f11, f5
15 addf f5, f6 , f5
16 sf 0(r2) , f5 Leyenda:
17 addi r1, r1, 4 Dependencias reales.
Dependencias ficticias.
18 addi r2, r2, 4
19 slti r3, r2, a
20 bnez r3, bucle

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


b) arrayy[i] = arrayy[i] + a * arrayx[i];

Sí, se puede desenrollar.


Sí, se puede ejecutar en paralelo.
c) Se trata de un bucle tipo SAXPY, por lo que las 25 iteraciones se pueden desenrollar.
(Tenemos en cuenta los 5 casos del Algoritmo de Tomasulo)

215
Tabla de Resultados:

RS Busy RSop1 Op1 RSop2 Op2 State Res Finished


STORED01 True - 100 ADDFP01 0 0 0 True
MULFP01 True - 3 - 1 6 0 True
ADDFP01 True - 3 MULFP01 0 0 0 True
d)
- MULFP01 está ejecutando la multiplicación, cuando termine se lo pasa a ADDFP01.
- ADDFP01 está esperando el resultado de MULFP01, cuando termine realizará la suma
y enviará el resultado a STORE01.
- STORE01 está esperando el resultado de ADDFP01 para almacenarlo en memoria.

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
lf f5, 0(r1) IF IS EX WB
lf f6, 0(r2) IF IS EX WB
multf f5, f11, f5 IF IS EX1 EX2 EX3 EX4 WB
addf f5, f6 , f5 IF IS ○ ○ ○ ○ EX1 EX2 WB
e) sf 0(r2) , f5 IF IS ○ ○ ○ ○ ○ ○ EX WB
addi r1, r1, 4 IF IS EX1 ○ WB
addi r2, r2, 4 IF IS EX1 ○ WB
slti r3, r2, a IF IS ○ ○ EX WB
bnez r3, bucle IF IS ○ ○ ○ EX WB

f) La cadena crítica de ejecución viene definida por las instrucciones: MULT → ADDF → SF

Cálculo del CPIreal del bucle en el estacionario, esto es, desde la fase IS del primer LF de una
iteración, hasta la fase IS del primer LF de la siguiente iteración, viene definido como:
g)
ciclos 9
CPIreal = = = 1
instrucciones 9

Aun habiendo sólo hay una R.S. de Store, el procesador no se bloquea porque esta se libera justo
h)
cuando se vuelve a necesitar, como podemos apreciar en antes de la etapa 19.

Del ciclo 25 al 40:


i) 1 15 15 1
CPIideal = ; CPIreal = = 0' 8333; CPIreal - CPIideal = CPIbloqueo → CPIbloqueo = - = 0'5
3 18 18 3

Del ciclo 20 al 35:


j) 1 15 15 1
CPIideal = ; CPIreal = = 0' 8333; CPIreal - CPIideal = CPIbloqueo → CPIbloqueo = - = 0'666
6 18 18 3

El CPIreal es el mismo para altos grados de superescalaridad (Issue Width) porque sólo
k) disponemos de una Estación de Reserva de LF y, hasta que no se libera el LF de la primera
iteración, no puede utilizarse el de la siguiente iteración.

l)

216
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 3b (VS) – Procesadores superescalares. Límites del ILP.

Objetivos.

En esta práctica los alumnos estudiarán los límites del ILP que se pueden conseguir con técnicas dinámicas en
GPP. Los alumnos repartidos en distintos grupos, observarán diferentes casos en los que prácticamente se
alcanza el límite del flujo de datos (data-flow limit), de manera que el tiempo medio por iteración de un pequeño
bucle es debido a tal límite. Los alumnos tendrán que exponer los resultados de esta práctica en el laboratorio
(ver último apartado: “Presentaciones de los resultados de la práctica”)

Preparación.

A cada uno de los grupos (de 2 o 3 alumnos) se le asignará un problema a resolver. Deberán ejecutar y analizar
los resultados tanto del código que se le haya asignado (escribir en la función problem) como del código de
ejemplo suministrado (función example).

Realización de la práctica.

Cada grupo deberá resolver el problema asignado y descubrir si su problema es totalmente “paralelizable” y no
tiene saltos condicionales dentro del bucle ni instrucciones que puedan bloquear la CPU. Deberá también
analizar el comportamiento del código proporcionado como ejemplo y explicar las diferencias obtenidas.

Se deben recoger los resultados siguientes:

 Máquina (sobre todo procesador) donde se ejecuta el código. Se aconseja usar CPU-Z (hay una copia de
Nov.2012 en ev.us.es, o también descargar de http://www.cpuid.com/softwares/cpu-z.html), el cual indica
muchas de las características del procesador.

 Compilador y Opciones de optimización usadas (sólo si algún grupo usa otro que no sea el del proyecto
suministrado).

 Códigos de C usados en los bucles.

 Duración en ciclos por iteración (del for) de todos los bucles.

 Códigos de Ensamblador del x86 que crean interesantes para explicar los resultados de tiempo.

 Realizar pruebas con tamaños mayores de vectores (cambiar valor de la constante N_ITER).

 Cualquier otro resultado que considere importante.

Con todo lo anterior, se deberá redactar una presentación (con Power Point u OpenOffice Impress) de
aproximadamente 8 minutos de duración, dividida en 2 partes aproximadamente iguales.

En la presentación, explicar la causa de la duración y las variaciones (si las hay) que se encuentran al cambiar de
código y al aumentar N_ITER. Y cualquier otra circunstancia que considere importante.

217
Problemas.

Para cada problema, el bucle debe incluir:

1) Sumatorios de elementos de vectores. Atención: evitar que se produzcan desbordamientos (overflow) en las
variables float. Probar con:
a. Una sola suma.
b. Dos sumas sobre el mismo acumulador.
2) Por iteración se debe incluir divisiones donde el denominador tenga algún elemento de vectores. Atención:
evitar que se produzcan desbordamientos (overflow) en las variables float. Probar con:
a. Una sola división.
b. Dos divisiones.
3) Provocar muchos accesos a memoria por iteración, sin que haya operaciones de tipo acumulador (sumatorio,
producto múltiple, etc.), ni tampoco dependencias entre iteraciones (recurrencia). Probar con:
a. 4 accesos por iteración.
b. 8 accesos por iteración.
c. 12 accesos por iteración.
4) Debe tener una sentencia if(). En el cuerpo del if() hay que realizar algún tipo de operación aritmética con
vectores (sin que haya operaciones de tipo acumulador -sumatorio, producto múltiple, etc.-, ni tampoco
dependencias entre iteraciones–recurrencia-). Probar con dos condiciones del if():
a. La condición sea difícilmente predecible (por ejemplo Random).
b. La condición sea predecible por la BTB.

Notas:

- Ejecute todo el proyecto VS2010 (ver ev.us.es) en modo Release.


- Compruebe que las opciones de compilación son las mismas que las de la práctica anterior P3A.
- El proyecto que se da no usa OpenCV y por tanto, debería funcionar en cualquier PC Intel (no funciona
correctamente en procesadores AMD).
- La duración del código real dependerá de la máquina concreta y del estado de carga de la memoria y
CPU. No tenga abiertas muchas aplicaciones y ejecute varias veces para quedarse con el mínimo tiempo.

Presentaciones de los resultados de la práctica.

En la siguiente sesión, se sorteará qué 2 alumnos de cada grupo presentan (cada uno la mitad). Si el grupo fuera
de 3 alumnos, el tercero se quedará esperando las preguntas de los profesores. Por tanto, todos los alumnos
deben prepararse la presentación entera. Se proporciona a los alumnos en ev.us.es una plantilla sobre cómo se
van a evaluar las presentaciones (matriz de evaluación).

Importante.

Si se usa VS 2012 o VS 2013, el compilador trabaja por defecto con las instrucciones del núcleo vectorial o
multimedia SSE2 o AVX. Es decir, puede generar a veces código con instrucciones de sufijo ps (como addps, mulps,
movaps, etc. cada una computa 4 elementos por instrucción en paralelo, usando DLP).

Para esta práctica, eso conduce a tiempos muy pequeños. Es preferible, elegir en: Propiedades del proyecto →
C → Generación de código, la opción de usar Instrucciones SSE (no poner SSE2). De esa forma, se usan
instrucciones escalares con sufijo ss y los resultados son más acordes con lo que se espera (al procesar solo con
un elemento por iteración)
218
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Solución:

Objetivo:

Estudio de los casos en los que prácticamente se alcanza el límite del flujo de datos (data-flow limit), de
manera que el tiempo medio por iteración de un pequeño bucle es debido a tal límite.

Datos del PC sobre el que se ejecutan las pruebas:

- Procesador: Intel Core 2 Duo E6550


- Core Speed: 2333 MHz
- Bus Speed: 333 MHz
- Memory Size: 4 Gbytes
- Caché: 2 x 32 Kbytes

Parámetros de configuración de Visual Studio 2013:

 Generación de código:
- Modelo de punto flotante: Rápido (/fp:fast)

 Optimización:
- Optimización: Optimización completa (/Ox)
- Tamaño o velocidad: Favorecer código rápido ( /Ot)

219
Apartado a) Una sola suma.

Código fuente

float problem () float example ()


{ {
int i; int i;
float z = 0.0; for (i=0; i<N_ITER; i++)
for (i=0; i<N_ITER; i++)
{
{
z = z + a[i]; a[i] = b[i] + 8;
} }
return z; return a[N_ITER-1];
} }

Desenrollado del bucle:


for (i=0; i<N_ITER; i++)
{
z = z + a[i];
00E724E0 addss xmm1,dword ptr [eax-4]
00E724E5 addss xmm1,dword ptr [eax]
00E724E9 addss xmm1,dword ptr [eax+4]
00E724EE addss xmm1,dword ptr [eax+8]
00E724F3 addss xmm1,dword ptr [eax+0Ch]
00E724F8 addss xmm1,dword ptr [eax+10h]
00E724FD addss xmm1,dword ptr [eax+14h]
00E72502 addss xmm1,dword ptr [eax+18h]
00E72507 add eax,20h
00E7250A cmp eax,0E7CF1Ch
}

N_ITER prueba a example


256 8,96875 1,23047
512 9,03711 1,25781
1024 9,03027 1,92090
2048 9,02686 1,34668
4096 14,42040 1,89355
8192 13,13010 1,85425
16384 13,85000 2,53613
32768 13,92180 2,92151
65536 14,15690 3,53685
131072 14,17040 6,59070
262144 13,94670 8,51536
524288 14,20740 10,00260
1048576 14,29440 9,78920
2097152 14,34960 9,78308
4194304 14,40960 9,57838
8388608 14,43750 9,47732
16777216 14,52370 9,45238

220
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Apartado b) Dos sumas sobre el mismo acumulador.

Código fuente

float problem () float example ()


{ {
int i; int i;
float z = 0.0; for (i=0; i<N_ITER; i++)
for (i=0; i<N_ITER; i++)
{ {
z = z + 1.0 + a[i]; a[i] = b[i] + 8;
} }
return z; return a[N_ITER-1];
} }

Desenrollado del bucle:


for (i=0; i<N_ITER; i++)
{
z = z + 1.0 + a[i];
001113E2 fld dword ptr [eax-4]
001113E5 fld dword ptr [z]
001113E8 fadd st,st(2)
001113EA faddp st(1),st
001113EC fstp dword ptr [z]
001113EF fld dword ptr [z]
001113F2 fadd st,st(1)
001113F4 fadd dword ptr [eax]
001113F6 fstp dword ptr [z]
001113F9 fld dword ptr [z]
}

N_ITER prueba a example


256 12,11330 1,23047
512 12,11330 1,25781
1024 12,03130 1,92090
2048 17,44190 1,34668
4096 17,50000 1,89355
8192 17,61110 1,85425
16384 18,31990 2,53613
32768 18,81400 2,92151
65536 18,75160 3,53685
131072 18,66810 6,59070
262144 18,83950 8,51536
524288 18,94900 10,00260
1048576 18,99390 9,78920
2097152 19,17390 9,78308
4194304 19,22660 9,57838
8388608 19,37980 9,47732
16777216 19,41450 9,45238

221
222
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 3b (DIG) – Planificación dinámica y Procesadores superescalares. Bucles vectoriales.

Objetivos.

En esta práctica los alumnos estudiarán los límites de ILP que se pueden conseguir con técnicas dinámicas en
procesadores superescalares.

Para ello comprobarán cómo se ejecuta la cadena de diversos códigos ASM usando SUPERTOMASIM
(SUPERescalar TOMasulo SIMulator).

Se observarán diferentes casos en los que prácticamente se alcanza el límite del flujo de datos (data-flow limit),
de manera que el número de ciclos (tiempo medio) por iteración de un pequeño bucle es debido a tal límite.

Los alumnos tendrán que exponer los resultados de esta práctica en el laboratorio (ver último apartado
“Presentaciones de los resultados de la práctica”.)

Preparación.

Se dan 4 códigos ASM. Cada grupo tendrá que trabajar sobre dos códigos ASM que le asignará el profesor de
prácticas (como hay 4 códigos, de dos en dos salen 6 combinaciones posibles).

Son bucles simples para ver el máximo IPC que se puede conseguir.

En la carpeta de ev3.us.es están los códigos ASM .s.

ASDP3B_TOMASIM1.s
ASDP3B_TOMASIM3.s
ASDP3B_TOMASIM4.s
ASDP3B_TOMASIM5.s

Se recomienda trabajar con las siguientes configuraciones que van de menor a mayor grado de superescalaridad
(m = 1, 3, 6), con objeto de entender el código primero y luego que se vea más fácilmente como se alcanza el
límite de ILP. No obstante, los alumnos pueden usar otras configuraciones. Por ejemplo, una vez que sepan cuál
es la instrucción que causa el bloqueo, pueden reducir el número de R.S. de tal instrucción.

asdp3b_configuracion_a.xml (m = 1)
asdp3b_configuracion_b.xml (m = 3)
asdp3b_configuracion_c.xml (m = 6)

223
Las configuraciones que se dan son estas:

asdp3b_configuracion_a.xml (m = 1)

asdp3b_configuracion_b.xml (m = 3)

asdp3b_configuracion_c.xml (m = 6)

224
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Importante:

En el Simulador SUPERTOMASIM, las F.U. no están segmentadas nunca, por tanto, para emular la segmentación,
se puede configurar para que tenga tantas F.U. como latencia tiene la operación (por ejemplo, la FU MULT en la
configuración asdp3b_configuracion_c.xml). Por otra parte, si el número de Unidades Funcionales es mayor que
la latencia, se podrían ejecutar varias operaciones por ciclo (por ejemplo, la Unidad Funcional INT en la
configuración de arriba asdp3b_configuracion_c.xml).

De esa forma, si se ejecutan varias instrucciones juntas para una operación con única Unidad Funcional (aunque
fueran independientes), tendríamos:
OPER1 IF IS D1 D2 D3 D4 D5 WB
OPER2 IF IS o o o o o D1 D2 D3 D4 D5 WB
OPER3 IF IS o o o o o o o o o o D1 D2 D3 D4 D5 WB

Sin embargo, para otra operación donde el número de Unidades Funcionales es igual que la duración, tendríamos
si se ejecutan varias instrucciones juntas (sin dependencia real):
OPER1 IF IS D1 D2 D3 D4 D5 WB // está usando la Unidad Funcional #1
OPER2 IF IS D1 D2 D3 D4 D5 WB // está usando la Unidad Funcional #2
OPER3 IF IS D1 D2 D3 D4 D5 WB // está usando la Unidad Funcional #3
OPER4 IF IS D1 D2 D3 D4 D5 WB // está usando la Unidad Funcional #4
OPER5 IF IS D1 D2 D3 D4 D5 WB // está usando la Unidad Funcional #5

Realización de la práctica.

Con el Simulador SUPERTOMASIM, se aconseja en general probar primero con un procesador escalar con pocos
recursos (RS, FU, etc.), para comprender el código y el funcionamiento (grado superescalaridad = 1). Por ejemplo
con la configuración del primer fichero xml.

Después, usar la configuración del superescalar (la que se indica arriba para cada código o alguna similar), en la
que a pesar de que tiene muchos recursos, surjan rápidamente bloqueos y se vea fácilmente qué instrucción es
la que los empiezan a producir. Con ello, deducir cuál el límite de ILP y su causa.

Recordar que para saber qué instrucción es la causa de los bloqueos hay que buscar la primera que tiene varias
fases IF en su cronograma: IF IF IF IS …

225
Nota:

El grupo que tiene que estudiar los saltos del if() y pensar cuando la BTB acertará en la predicción. Para hallar
la duración media de una iteración, halle la media ponderada de dos iteraciones (una en la que la BTB acierta y
otra en la que falla).

Aunque el formato de la presentación es libre, se detallan algunos consejos sobre la realización y los resultados
buscados. Se deben recoger los resultados de ejecución. Utilice tablas con los resultados que obtenga. Piense y
resuelva por ejemplo preguntas sobre los códigos ASM dados, su ejecución y límite de ILP como estas:

a) ¿Qué hacen?

b) ¿Cómo se podrían escribir en código de alto nivel?

c) ¿Son vectorizables directamente? ¿Se podrían vectorizar si se hicieran cambios de alto nivel?

- Para los códigos que usan saltos dentro del bucle, además hay que pensar en estas preguntas:
- Como la BTB de SUPERTOMASIM predice siempre Tomada, ¿cuándo acertará en los diversos saltos?
- Si la BTB fuera muy sofisticada, ¿acertará en los diversos saltos?

d) ¿Cuánto valen los diversos CPI en las iteraciones 2 y 3?

e) ¿Cuánto valen los diversos CPI en el estacionario (recordar cómo medirlo según práctica anterior P3A)?

f) ¿Vale igual el CPI real en el estacionario para un RISC Escalar y uno Superescalar?

g) Duración en ciclos por iteración de todos los bucles

h) ¿Para m = 1, la reordenación mejoraría las prestaciones?

i) ¿Para m altos, la reordenación mejoraría las prestaciones?

j) Identificar la causa del CPI bloqueo, e intentar calcular teóricamente por qué sale eso.

k) etc. etc.

Si algún alumno quiere probar una reordenación en SUPERTOMASIM, debe cortar y pegar líneas completas del
ASM para evitar fallos del simulador (ver apéndice para más detalle).

Con todo lo anterior, se deberá redactar una presentación (estilo Power Point u OpenOffice Impress) de
aproximadamente 10 minutos de duración, dividida en 2 partes aproximadamente iguales en duración
(aproximadamente 5 minutos).

Presentaciones de los resultados de la práctica.

En la sesión, se sorteará qué alumno de cada grupo presenta primero y cuál después (cada uno la mitad). Por
tanto, todos los alumnos deben prepararse la presentación entera. Se dejará en ev.us.es una plantilla sobre cómo
se van a evaluar las presentaciones (matriz de evaluación).

Todos los alumnos oyentes, deben aprender de lo que exponen sus compañeros, pues al final de esta sesión se
hará un test de evaluación ev3.us.es. Además el profesor de prácticas podrá valorar positivamente las críticas o
errores que un oyente encuentre sobre las exposiciones de otros.

226
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 3b (DIG) – Planificación dinámica y Procesadores superescalares. Ejercicio en grupo.

Dado el siguiente código de alto nivel:


int i;
double x[N_ITER], y[N_ITER], z[N_ITER]; // vectores consecutivos en memoria.
for (i=0; i<N_ITER; i++)
acum = acum + x[i] + y[i] / z[i];

Suponer que:

- Las etapas a considerar son IS EX WB.

- Superescalar de grado m = 5.

- Frecuencia = 3 GHz

- Suponer que la fase EX de la división tarda 6 ciclos. Es Unidad Funcional única y no está segmentada.

- Suponer que la fase EX de los Load/Store tarda 2 ciclos. Hay dos puertos para caché L1 (dos Unidades
Funcionales) y están segmentadas.

- El resto de operaciones con fase EX de sólo 1 ciclo y tienen infinitas Unidades Funcionales.

- La memoria RAM dispone de un Ancho de Banda de 8 GB/s.

Responder:

a) Escribir el código (para un RISC). Por comodidad usar un único registro base Rb, modificando los
desplazamientos según el array. Los desplazamientos (offsets) respecto del registro base Rb serían:
(Rb)8*N_ITER, (Rb)16*N_ITER, para y[i], z[i] respectivamente.

b) Dibujar el cronograma de la primera iteración. Suponer BTB que siempre acierta y tiene precargadas
suficientes instrucciones, para que la fase IS pueda emitir hasta 5 instrucciones.

c) Calcular el CPI para el estacionario para N_ITER pequeño (todo vector cabe en caché L1) y para N_ITER
grande (todo acceso a vectores se hace a RAM). Justificar los resultados.

227
Solución:

Apartado a)

La idea principal de lo que se va a realizar es cargar cada elemento de vector en Dx, Dy y Dz, realizar las operaciones
correspondientes, y saltar. Obviamos la instrucción de store ya que se podría realizar al final del bucle y usar el
registro Dacum como temporal hasta el final de la operación. Suponemos todos los vectores y datos ya
precargados en registros y reordenamos el código para obtener menor número de ciclos de espera.
bucle:
LD (Rb)16, Dz
LD (Rb)8 , Dy
LD (Rb)0 , Dx
DIVD Dy, Dy, Dz
ADDI Rb, Rb, 8
ADDD Dacum, Dacum, Dx
ADDD Dacum, Dacum, Dy
SLT Rc, Rb, Rfinx
BNEZ Rc, bucle

Apartado b)

Observamos la primera iteración al completo. Como se puede apreciar, se producen ciertos ciclos de espera que
no afectan al CPI ya que quedan enmascarados por la operación DIVD.

En la siguiente captura observan las dos primeras iteraciones, donde podemos apreciar que el CPI vendrá
determinado por la distancia entre los WB de cada DIVD. Se denota por EX.L1 y EX.L2 a cada una de las unidades
funcionales para los LOAD/STORE.

228
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Apartado c)

1
Como el procesador se supone tiene escalabilidad m = 5, entonces sabemos que su CPI es: 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 =
5

Procedemos a calcular el CPI de bloqueo. Para lo que necesitamos conocer el CPI real, luego:
𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 − 𝐶𝑃𝐼𝑅𝑒𝑎𝑙

𝑛ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑐𝑖𝑐𝑙𝑜𝑠 6
𝐶𝑃𝐼𝑅𝑒𝑎𝑙 = = = 0′6̂
𝑛ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝑖𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠 9

El número de ciclos es 6 porque entre DIVD y DIVD se desfasan entre iteración e iteración 6 ciclos de reloj, ya que
el resto de ciclos de esperas, quedan enmascarados por esta operación. Luego nuestro CPI de bloqueo será:
6 1 7
𝐶𝑃𝐼𝐵𝑙𝑜𝑞𝑢𝑒𝑜 = − = = 0′46̂
9 5 15

El CPI Real calculado es el correspondiente para un N Iter suficientemente pequeño para que sólo se acceda a caché,
y no se produzcan retrasos o esperas por accesos a memoria.

Para un N Iter suficientemente grande que requiera de constantes accesos a memoria por fallos de caché (tanto
forzosos como por conflicto) necesitamos del uso del ancho de banda de la memoria dado en el enunciado.

𝑁ú𝑚𝑒𝑟𝑜 𝑑𝑒 𝐵𝑦𝑡𝑒𝑠 𝑐𝑜𝑛𝑠𝑢𝑚𝑖𝑑𝑜𝑠


Sabemos que: 𝐴𝐵 = 𝑇𝑖𝑒𝑚𝑝𝑜 𝑑𝑒 𝑒𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛

Calculamos a continuación el tiempo de ejecución para sustituirlo en dicha fórmula; de la que desconocemos
aún el CPI Real.
𝑡𝑒𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛 = 𝐶𝑃𝐼𝑅𝑒𝑎𝑙 ∙ 𝑇 ∙ 𝑁º𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑐𝑖𝑜𝑛𝑒𝑠

𝑡𝑒𝑗𝑒𝑐𝑢𝑐𝑖ó𝑛 = 𝐶𝑃𝐼𝑅𝑒𝑎𝑙 ∙ (𝑁𝐼𝑡𝑒𝑟 ∙ 9)

Ahora, calculamos los bytes consumidos, que son 8 Bytes por acceso en tres accesos, durante N Iter iteraciones:

𝐵𝑦𝑡𝑒𝑠 𝑐𝑜𝑛𝑠𝑢𝑚𝑖𝑑𝑜𝑠 = 8 𝐵𝑦𝑡𝑒𝑠 ∙ 3 𝑎𝑐𝑐𝑒𝑠𝑜𝑠 ∙ 𝑁𝐼𝑡𝑒𝑟

Conociendo el ancho de banda de la memoria, podemos sustituir en la fórmula del AB y despejar el CPI Real.:

24 ∙ 𝑁𝐼𝑡𝑒𝑟 24 ∙ 3 ∙ 109
𝐴𝐵 = = = 8 ∙ 109
𝐶𝑃𝐼𝑅𝑒𝑎𝑙 ∙ 𝑇 ∙ (𝑁𝐼𝑡𝑒𝑟 ∙ 9) 𝐶𝑃𝐼𝑅𝑒𝑎𝑙 ∙ 9

Quedando por tanto: CPI Real. = 1


229
230
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Test 1.

Parte Preguntas Generales.

 Si un procesador tiene emisión (IS, Issue) dinámica, eso significa que:

 Ninguna de las otras es correcta.

□ El procesador no emite las instrucciones nunca.

□ El compilador prepara las instrucciones para que las emita el procesador sin comprobar nada.

□ El procesador prepara las instrucciones reordenándolas antes de emitirlas.

 Para un procesador con Algoritmo de Tomasulo, predicción y especulación dinámicas, ¿qué características
tiene?:

 In-order-issue (emisión en orden).

□ Out-of-order-issue (emisión fuera de orden).

□ In-order-execution (ejecución en orden).

 Out-of-order-execution (ejecución fuera de orden).

 Para un RISC con Algoritmo de Tomasulo y fases IF IS EX WB, qué frase/s son verdad para su BTB:

□ Se predice un salto en la fase IS, se busca la instrucción predicha y se resuelve en tal ciclo.

 Se predice un salto en la fase IF y se resuelve en EX.

 Se busca la instrucción predicha en un ciclo después de IF.

□ Se predice un salto en la fase IF, y se resuelve el salto también en IF.

□ Se busca la instrucción predicha en la fase EX.

231
Parte: Rendimiento con Visual Studio.

 ¿Qué ocurre al ejecutar este código en un procesador moderno para un valor alto de N_ITER?
for (i=0; i<N_ITER; i++) a[i] = b[i] + 8;

 Al ser un código fácilmente paralelizable la ejecución es casi óptima.


□ Si N_ITER es demasiado grande empiezan a aparecer bloqueos debido a la acumulación de muchas
sumas.
□ El tiempo de ejecución está principalmente influido por los accesos a memoria al leer y escribir sobre
diferentes vectores.
□ Si N_ITER es demasiado grande empiezan a aparecer ciclos de bloqueo debido a la saturación de las
estaciones de reserva.

 ¿Qué ocurre al ejecutar este código en un procesador moderno para un valor alto de N_ITER?
for (i=0; i<N_ITER; i++) a[i] = 3 / b[i];

□ Dependiendo del valor de los elementos de b[i] podrán aparecer ciclos de bloqueo.
□ Al no existir dependencias entre iteraciones no aparece un número significativo de ciclos de bloqueo.
 La operación de división está no segmentada y requiere tantos ciclos que al cabo de algunas
iteraciones empiezan a aparecer ciclos de bloqueo.
□ El tiempo de ejecución está principalmente influido por los accesos a memoria al leer y escribir sobre
diferentes vectores.

 ¿Cuál es la causa de que al ejecutar el siguiente código, si N_ITER es grande, empiecen a aparecer ciclos
de bloqueo?
for (i=0; i<N_ITER; i++) a[i] = b[i] + c[i] + d[i] + e[i] + f[i] + g[i] + h[i];

□ La instrucción de suma no suele estar segmentada y requiere demasiados ciclos.


□ Las sumas repetidas impiden la ejecución paralela de las instrucciones.
 Se producen tantos accesos a memoria en cada iteración que se saturan las unidades funcionales
(puertos) de acceso a memoria.
□ Se producen accesos a tantos vectores diferentes que se producen fallos de cache.

 ¿Cuál es la causa de que al ejecutar el siguiente código, si N_ITER es grande (pero no se accede a RAM),
empiecen a aparecer ciclos de bloqueo?
for (i=0; i<N_ITER; i++) z += a[i];

□ Los saltos condicionales del bucle for generan muchos ciclos de bloqueo de control.
□ Los accesos a caché para acceder a los elementos del vector, suelen ser muy lentos (cientos de ciclos).
 La suma acumulada introduce dependencias reales de datos entre iteraciones.
□ La suma no suele estar segmentada, lo que acabará bloqueando en el estacionario.
232
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Parte: Implementación digital (SUPERTOMASIM).

Pregunta 1:

Dado el siguiente cronograma de dos iteraciones de un bucle, y sabiendo que la U.F. de DIVF no está segmentada
y dura 5 ciclos, la cual produce bloqueos totales que se muestran como IF IF IF IS (p ej a partir de C27), ¿cuál o
cuáles de las siguientes afirmaciones es (o son) verdadera?

 Si el procesador tuviera un grado de superescalaridad (m) mayor que 2, el CPI seguiría siendo el mismo.

□ Si el procesador tuviera un grado de superescalaridad (m) mayor que 2, el CPI se iría reduciendo
aproximadamente de forma proporcional con m.

□ Si el procesador tuviera un grado de superescalaridad (m) mayor que 2, el CPI se iría reduciendo, pero a
partir de m = 5, el CPI ya sería el mismo (pues la U.F. DIVF dura 5 ciclos).

Pregunta 2:

Dado el siguiente cronograma, y sabiendo que el contenido inicial de los registros es F0 = 0, F5 = 5 y F6 = 6. El


estado de las R.S. de las dos primeras ADDF en el ciclo C28 (cuando acaba EX de la primera ADDF) sería:

 La primera ADDF contiene el resultado 11, y los registros fuentes con valores 5, 6. La segunda ADDF no
contiene ningún resultado, y sólo un registro fuente con valor 5 (el otro aún no le ha llegado).

□ La primera ADDF aún no contiene el resultado (pues no ha hecho WB), y los registros fuentes con valores
5, 6. La segunda ADDF no contiene ningún resultado, pero sí los valores de los dos registros fuentes (5 y
11 que es la suma de 5+6).

□ La primera ADDF contiene el resultado 11, pero no los valores de los registros fuentes, pues tal R.S. se ha
liberado ya. La segunda ADDF no contiene ningún resultado, pero sí los valores de los dos registros fuentes
(5 y 11 que es la suma de 5+6).
233
Pregunta 3:

Dado el siguiente cronograma, y sabiendo que el contenido inicial de los registros es F0 = 0, F5 = 5 y F6 = 6, ¿cuál
o cuáles de las siguientes afirmaciones es verdadera?

 El cronograma es correcto, y la segunda ADDF (que empieza a ejecutarse en el ciclo C30), tendría que
guardar el valor de F5 en algún campo de la R.S. Se necesitarán tres R.S. libres de ADDF.

□ El cronograma es incorrecto porque la tercera ADDF no puede ejecutarse a partir del ciclo C27, ya que
tiene que escribir sobre el registro F5, que lo necesita la segunda ADDF (que aún no ha empezado).

□ El cronograma es correcto y sólo se necesitarán dos R.S. libres de ADDF, ya que la segunda ADDF usaría la
R.S. de la primera (que aún no ha empezado).

□ El cronograma es incorrecto porque la segunda ADDF podría haber empezado a ejecutarse a partir del
ciclo C26.

234
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 4 – Rendimiento utilizando el núcleo vectorial.

Objetivos y preparación.

En esta práctica vamos a estudiar la optimización de código mediante el uso de las extensiones multimedia o
núcleos vectoriales disponibles en los procesadores PIV.

La historia de las extensiones multimedia en la familia x86 ha sido resumidamente:

- Comienza con el Pentium MMX (1994), en el que se añadieron un conjunto de 8 registros de 64 bits
(2x32 bits) y una serie de instrucciones que permitían el procesado SIMD (Single Instruction Múltiple
Data) de datos enteros. Estas extensiones se denominaron MMX.

- El siguiente paso se da en el Pentium III, el cual contiene un conjunto de 8 registros de 128 bits (4x32
bits) y una serie de instrucciones para el procesado SIMD de flotantes en simple precisión. A estas
instrucciones se les denomina SSE (Streamming SIMD Extensions).

- Posteriormente, en el P4 se incluyen instrucciones (SSE2 y SSE3) para poder emplear los registros SSE
con datos enteros y flotantes de simple o doble precisión (4x32bits o 2x64 bits).

Un detalle significativo de la implementación de las SSE, es que en el PIII cualquier operación cuyos operandos
sean 4 flotantes de simple precisión, tiene una duración equivalente a dos operaciones escalares; mientras que
en el PIV la duración es equivalente a la de procesar un único escalar.

Tanto los niveles de caché como su tamaño, número de vías y tamaño de la línea tendrán una influencia muy
importante en el rendimiento de las pruebas que haremos en esta práctica.

Antes de acudir al laboratorio el alumno deberá:

 Entender cómo está organizado el código que se proporciona con este enunciado.

 Conocer cómo se ubican en memoria cada una las variables que aparecen en éste en base a sus
declaraciones y entender el subconjunto de instrucciones multimedia IA32 que se proporcionan.

Estudio previo:

 Antes de comenzar la práctica, traer un diagrama de L1, antes de la ejecución de init, tras su ejecución
y tras la ejecución del bucle SAXPY, para un número de elementos de x[] e y[] igual al que ocupan en
una vía de L1, para el doble de ésta, … y así hasta el número de elementos que llene y dobla dicha caché.

 Se aconseja traer un resumen del código, desde un punto de vista funcional.

235
A continuación se presentan las instrucciones IA32 que serán necesarias para la realización de la práctica, con su
referencia a “IA32 Intel Architecture Software Developer’s Manual. Volume 2: Instruction Set Reference”. Nos
restringiremos a:

- Todos los operandos en registro o memoria con los que trabajaremos serán de 32 bits (o de 128 para
registros xmm de 128 bits y accesos multimedia) aunque algunas instrucciones admiten otros tamaños
de operandos.

- Los operandos inmediatos pueden cambiar de tamaño, pero no afecta a los benchmark de esta práctica.

- Los direccionamientos que usaremos serán mayoritariamente: [r], [r+offset]

- Notación: r = registro; m = dirección de memoria; imm = constante inmediata

ARITMÉTICAS ENTERAS:
add r/m, r/m/imm[32,16 o 8] (Add pág 3.021)
lea r, m (Load Effective Address pág 3.372)
sbb r/m, r/m/imm[32,16 o 8] (Integer SuBtraction with Borrow pág 3.685)
sub r/m, r/m/imm[32,16 o 8] (Subtract pág 3.729)
cmp r, r/m/imm[32,16 o 8] (CoMPare two operands pág 3.082)

SALTOS:
jne etiqueta (Jump if Not Equal pág 3.352)

ACCESOS A MEMORIA:
mov r/m, r/m/imm[32,16 o 8] (Move pág 3.430).
Nota: Ambos operandos deben ser del mismo tamaño y no pueden referenciar a memoria.
movss xmm/m, xmm/m (Move Scalar Single-precision floating-point values pág 3.483)
movaps xmm/m128, xmm/m128 (Move Aligned Packed Single-precision floating-point values pág 3.441)

ARITMÉTICAS MULTIMEDIA:
addps xmm, xmm/m (Add Packed Single-precision floating-point values pág 3.025)
addss xmm, xmm/m (Add Scalar Single-precision floating-point values pág 3.029)
mulps xmm,xmm/m128 (Multiply Scalar Single-precision floating-point values pág 3.496)
mulss xmm, xmm/m (Multiply Scalar Single-precision floating-point values pág 3.500)
shufps xmm, xmm/m128, imm8 (Shuffle Packed Single-precision floating-point values pág 3.703)
sqrtps xmm, xmm/m128 (Computate Square Roots of Packed Single-precision floating-points values – pág 3.713)

Como registros de programación, podrá hacer uso de eax, ebx, ecx, edx, esi y edi de 32 bits y xmm0, ... xmm7.

NOTA: la instrucción cmp no se debe separar, en general, del salto jne, pues en los x86 hay un bit de estado que
guarda el resultado de la última operación aritmética (de forma que si entre cmp y jne hubiese otra
instrucción aritmética, tal bit podría ser modificado y perderse el resultado de la comparación).

236
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Realización de la práctica:

PRUEBA 1: Comparación de las implementaciones (para tamaño fijo de los vectores).

En toda esta prueba, la constante N debe valer 1, de forma que el tamaño de los vectores será VSIZE.

a) Abrir el proyecto que se proporciona y verificar que se encuentran activas las siguientes opciones de
compilación en: Propiedades de configuración → C/C++ →
- General: Formato de la información de depuración → Base datos de prog. /Zi,
- Optimización → Optimización: Optimización completa (/Ox)
- Optimización → Tamaño o velocidad: Favorecer código rápido (Ot).
- Generación de código → Comprobaciones básicas en tiempo de ejecución: Predeterminado
- Generación de código → Habilitar conjunto de instrucciones mejorado: SSE2 (/arch:SSE2)
- Generación de código → Modelo de punto flotante: Rápido (/fp:fast)

b) Valorar VSIZE para que ambos vectores ocupen 4 KB.

c) Implementar el bucle SAXPY de tres formas: en lenguaje C (ya se da hecho), en ensamblador haciendo
uso de las instrucciones multimedia que operan sobre un único dato (sufijo ss), y en ensamblador
haciendo uso de las que operan sobre múltiples datos (sufijo ps); para sus correspondientes funciones:
codigo_c (ya se da hecho), codigo_sse_escalar y codigo_sse_vectorial, respectivamente. Para insertar
instrucciones en ensamblador dentro del código C (ensamblado en línea) puede utilizar como referencia
la función init(…) del proyecto.

d) Para ejecutar el programa utilice Depurar Iniciar sin depurar (o CTRL+F5). Si ha implementado de forma
incorrecta las funciones solicitadas anteriormente aparecerá el mensaje WRONG RESULTS: check your
code durante la ejecución. Una vez comprobada las implementaciones rellene la tabla de resultados.

PRUEBA 2: La memoria caché y el tamaño de los vectores procesados.

a) Dar a la constante N el valor adecuado para que la suma de los tamaños de ambos vectores x[], y[] sea
8 MB y responder las preguntas de la hoja de resultados.

b) Usar CPU-Z para comprobar los tamaños de caché de las máquinas y comprobar que los saltos en
tiempos de ejecución se han producido al desbordar las cachés

237
Memoria voluntaria:

Se proponen las siguientes pruebas. El alumno deberá entregar al menos dos de ellas como memoria voluntaria.

Tenga en cuenta que los resultados van a ser muy diferentes con otro procesador, porque podría incluir diferentes
optimizaciones en hardware (por ejemplo caché de trazas, prebúsquedas de datos si el patrón de acceso es fácil
de adivinar como ocurre en los bucles SAXPY o similares, etc.):

 Comprobar en el modo Debug, qué código genera el compilador Visual.Net y comentar como funciona
éste. A continuación, activar los flags de optimización del compilador para generar código rápido y anotar
resultados. Comprobar cómo ha cambiado el código ensamblador generado y pensar en las
optimizaciones que ha hecho el compilador.

 Desenrollar en ensamblador dos iteraciones para el código SSE vectorial (se procesarían 8 elementos por
iteración) y para el código SSE escalar, entrelazando y reordenando. Anotar la aceleración conseguida
para diferentes tamaños.

 Probar con diferentes zancadas de recorrido de los vectores (en lugar de i++ en el bucle for, usar i+=2,
i+=4, etc.). De esa forma el Miss rate cambia, y deben notarse diferencias mayores cuando los vectores
no caben en los diferentes niveles de cachés.

 Realizar las diferentes pruebas con otro procesador más moderno, justificando los resultados en función
de las optimizaciones de la nueva arquitectura.

238
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Código fuente final:

using namespace std;

#include <iostream>
#include "QueryPerformanceTiming_rdtsc.h"

#define N_REPETITIONS 1024


//Especifique el número de elementos para los que ambos vectores ocupen 4 KB */
#define VSIZE 1024
#define N 1
#define TAM VSIZE*N

void codigo_c (int tam);


bool codigo_sse_escalar (int tam);
bool codigo_sse_vectorial (int tam);

//inicialización con instr. SSE: x[] = {1, 2, 3, 4, 1, 2, 3, 4,.. }, y[i] = sqrt(x[i])


void init(int tam);

//Funciones para comprobar si los resultados son correctos


bool checkCode(int tam);

// Alinea los vectores en frontera de 16 bytes.


__declspec(align(16)) float x[TAM], y[TAM], m[4] = {1.0, 2.0, 3.0, 4.0};
float results[TAM]; //vector para comprobar resultados
float a = (float)3.14159; // Constante para los bucles
QPTimer c1, c2, c3;

//***********************************************************************
//inicialización con instr. SSE: x[]={1,2,3,4,1,2,3,4...}, y[i]=sqrt(x[i])
void init (int tam) {
__asm {
lea eax, m
lea ebx, x
lea edx, y
mov edi, tam
shl edi, 2
mov ecx, ebx
add ecx, edi
movaps xmm0, [eax] //LD
sqrtps xmm1, xmm0
lp:
movaps [ebx], xmm0 //ST
movaps [edx], xmm1
add ebx, 16
add edx, 16
cmp ebx, ecx
jne lp
}
}

//***********************************************************************
bool checkCode (int tam) {
int i = 0;
while (fabs (results[i] - y[i]) < 1e-6 && i<tam)
i++;
if (i != tam) {
cout << " -> WRONG RESULTS: check your code\n" << endl;
return false;
}
return true;
}
239
//***********************************************************************
int main(void) {
int i;

c1.Calibrate();
c2.Calibrate();
c3.Calibrate();

for (i=1; i<=N; i*=2) {


cout << "*** C Code for " << i*VSIZE << " elements ***" << endl;
codigo_c (i*VSIZE);
// cout << " First element: " << y[0] << " ;
// Last element: " << y[i*VSIZE-1] << endl ;
c1.PrintMinimumCycles (" Minimum time in cycles: "); cout << endl ;
c1.PrintMeanCycles (" Mean time in cycles: ");
c1.PrintMinimumCyclesByIteration(" Minimum time in cycles for an array element:
",i*VSIZE); cout << endl << endl;

cout << "*** SSE Scalar for " << i*VSIZE << " elements ***" << endl;
if (codigo_sse_escalar(i*VSIZE)) {
c2.PrintMinimumCycles (" Minimum time in cycles: "); cout << endl;
c2.PrintMeanCycles (" Mean time in cycles: ");
c2.PrintMinimumCyclesByIteration(" Minimum time in cycles for an array
element: ",i*VSIZE); cout << endl << endl ;
}

cout << "*** SSE Packet (vectorial) for " << i*VSIZE << " elements ***" << endl
;
if (codigo_sse_vectorial(i*VSIZE))
{ c3.PrintMinimumCycles (" Minimum time in cycles: "); cout << endl ;
c3.PrintMeanCycles (" Mean time in cycles: ");
c3.PrintMinimumCyclesByIteration(" Minimum time in cycles for an array
element: ",i*VSIZE); cout << endl << endl ;
}

c1.ResetAll();
c2.ResetAll();
c3.ResetAll();
}
return 0;
}

//***********************************************************************
// FUNCIONES
//***********************************************************************

//Código C en otra función para que no influyan las optimizaciones del compilador
void codigo_c (int tam) {
int i, j;

init (tam);
for (j=0; j<N_REPETITIONS; j++) {
c1.Start();
for (i=0; i<tam; i++)
{ // Código a probar
y[i] = a*x[i] + y[i];
}
c1.Stop();
c1.Reset();
}
//Guardo resultados correctos en vector results[]
for(i=0;i<tam;i++)
results[i] = y[i];
}
240
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

//***********************************************************************
//Código SSE escalar en ensamblador
bool codigo_sse_escalar (int tam) {
int i;
init (tam);
for (i=0; i<N_REPETITIONS; i++) {
c2.Start();
__asm {
lea eax, a
lea ebx, x
lea edx, y
mov edi, tam
lea ecx, [ebx+edi*4]
lea edi, y
movss xmm2, [eax] // escalar a parte menos significativa
// XXXXXXX EL alumno debe escribir aquí el Código SSE escalar del SAXPY
etiq_esc:
movss xmm0 , [ebx]
movss xmm1 , [edx]
mulss xmm0 , xmm2
addss xmm0 , xmm1
movss [edx], xmm0
add ebx , 4
add edx , 4
cmp ebx , ecx
jne etiq_esc
}
c2.Stop();
c2.Reset();
}
return checkCode(tam);
}
//***********************************************************************
// Código SSE vectorial en ensamblador
bool codigo_sse_vectorial (int tam) {
int i;
init (tam);
for (i=0; i<N_REPETITIONS; i++) {
c3.Start();
__asm {
lea eax, a
lea ebx, x
lea edx, y
mov edi, tam
lea ecx, [ebx+edi*4]
lea edi, y
movss xmm2, [eax] //escalar a parte menos significativa
shufps xmm2, xmm2, 0 //el elemento 0 se replica en todo xmm2
//XXXXXXX EL alumno debe escribir aquí el código SSE vectorial del SAXPY
etiq_vec:
movaps xmm0 , [ebx]
movaps xmm1 , [edx]
mulps xmm0 , xmm2
addps xmm0 , xmm1
movaps [edx], xmm0
add ebx , 16
add edx , 16
cmp ebx , ecx
jne etiq_vec
}
c3.Stop();
c3.Reset();
}
return checkCode(tam);
}
241
TABLA DE RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 4. RENDIMIENTO USANDO EL NÚCLEO VECTORIAL.

ALUMNO:

PRUEBA 1: Comparación de las implementaciones.

C Un único dato (ss) Múltiples datos (ps)

Tiempo en
1183 1351 343
ciclos
Ciclos por
1183 1351 343
Elemento = 2'3105 = 2'6386 = 0'6699
512 512 512
procesado
Aceleración
2'6386 2'6386
respecto del A = = 1'1420 A = = 3'9387
2'3105 0'6699
código 1.0
ensamblador C incrementa 14’2% PS incrementa 293’87%
escalar
19 Instr. 15 Instr. 16 Instr.
IPC = 2'0558 = 5'6848 = 5'9710
4∙2'3105 ciclo 2'6386 ciclo 4∙0'6699 ciclo

Número de elementos del vector x[] Fórmula de los ciclos por elemento procesado:

512 elementos. tiempo ciclos 1 prog T_ejec ciclos


x =
1 prog Eltos. procesados Eltos procesados (512)

Justificar cualitativa y brevemente la diferencia de tiempos.

El bucle en código C, aunque hace una operación sobre un dato, el compilador desenrolla 4 veces.
El código escalar trabaja también sobre un dato, pero sin desenrollar. Por este motivo, el código
C es más rápido. Si lo comparamos con PS, múltiples datos, este es el mejor, 4 datos al mismo
tiempo.

¿Qué diferencia habría entre el código que usa el núcleo vectorial (instrucciones sobre múltiples datos)
y el código surgido del desenrollado de 4 iteraciones usando instrucciones escalares?

Las instrucciones vectoriales son más “pesadas” que las instrucciones escalares, ya que
las vectoriales (128 bits) tratan 4 elementos al mismo tiempo.

242
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

PRUEBA 2: La memoria caché y el número de elementos a procesar.

Tamaño
que usan C , Escalar Vectorial C Escalar Vectorial
ambos Nº eltos
vectores de x[] MR, L1 MR, L2 MR, L1 MR, L2 ciclos/ ciclos/ ciclos/
Juntos (teórico) (teórico) (teórico) (teórico)
Acel. Acel. Acel.
eltos eltos eltos
(KBytes)
4 512 0,16 0 0’04 0 2,283200 1,1347305 2,590820 1,00 0,628906 4,119565
8 1024 0,16 0 0’04 0 2,262700 1,0483384 2,372070 1,00 0,611816 3,877095
16 2048 0,16 0 0’04 0 2,257570 1,0862983 2,452390 1,00 0,644287 3,806366
32 4096 0,16 0 0’04 0 2,403690 1,0970494 2,636960 1,00 1,159550 2,274134
64 8192 0,32 0 0’04 0 2,402400 1,0981682 2,638240 1,00 1,161250 2,271891
128 16384 0,32 0 0’08 0 2,402400 1,0989685 2,640170 1,00 1,171080 2,254469
256 32768 0,32 0 0’08 0 2,403900 1,0994401 2,642940 1,00 1,172360 2,254373
512 65536 0,32 0 0’08 0 2,405980 1,0998646 2,646260 1,00 1,179090 2,244316
1024 131072 0,32 0 0’08 0 2,437010 1,1022199 2,686120 1,00 1,189560 2,258081
2048 262144 0,32 0 0’08 0 2,549280 1,0905459 2,780100 1,00 1,466510 1,895736
4096 524288 0,32 0 0’08 0 5,134660 1,0052024 5,161370 1,00 5,104470 1,011148
8192 1048576 0,32 0 0’08 0 6,042040 1,0230682 6,181440 1,00 6,053060 1,021207
16384 2097152 0,32 0 0’08 0 6,408630 1,0014435 6,417870 1,00 6,277530 1,022355
32768 4194304 0,32 0 0’08 0 6,450150 1,0031012 6,470160 1,00 6,305160 1,026169
65536 8388608 0,32 0 0’08 0 6,489510 1,0028473 6,508000 1,00 6,350340 1,024826
131072 16777216 1 1 1 1 6,512760 1,0022239 6,527250 1,00 6,376280 1,023678

243
Acceso a L2

Acceso a RAM

¿Por qué cuando el tamaño de ambos vectores es L2 (teóricamente ambos vectores residirían en L2), se da ya
una degradación del rendimiento?
1 𝑓𝑙𝑜𝑎𝑡 → 4 𝐵𝑦𝑡𝑒𝑠 𝑐𝑖𝑐𝑙𝑜𝑠
8 } 𝑥 = 1024 ∙ 1024 → ≅ 2 𝑒𝑛 𝑡𝑜𝑑𝑜𝑠.
Porque acceder a las cachés
𝑥 𝑓𝑙𝑜𝑎𝑡 → 𝑀𝐵𝑦𝑡𝑒𝑠 𝑒𝑙𝑒𝑚𝑒𝑛𝑡𝑜𝑠 superiores requiere Cbloq memoria
2
¿Presentan una misma tasa de fallos de acceso a caché los códigos escalar y vectorial? ¿Por qué?

Ciclos/Elto Miss rate El Vectorial PS ha empeorado mucho más que los otros
Código C 1’619 2’02 respecto a Escalar SS por no tener que acceder a L2.
Escalar SS 2’090 2’10
El algoritmo vectorial se degrada más cuanto más
Vectorial PS 0’560 2’05
grandes son los vectores.

¿Tienen distinta penalización temporal (Pmiss) por fallo de caché ambos códigos? ¿Por qué?

Pmiss(Escalar SS) = 2' 10 - 2'09 = 0'01


No tienen la misma penalización {
Pmiss(Vectorial PS) = 2' 05 - 0'56 = 1'49
¿Cuál de los dos ve más degradado su comportamiento por los fallos de caché? ¿Por qué?

El caso vectorial es el más degradado.

244
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 5 – Repaso de problemas.

P0. Ejercicio de VLIW. Hallar el Tiempo de ejecución en un procesador VLIW con las siguientes propiedades:

- Sean M operaciones.
- Sea un procesador VLIW puro con 8 operaciones por macro-instrucción.
- El compilador rellena con un 20% las macros de NOOP.
- La frecuencia del procesador es de 1’5 GHz.
- El CPI MEM es de 0’2 ciclos de bloqueo por instrucción.

Solución:
TiempoEjecución = NúmeroInstrucciones ∙ CPI ∙ Periodo

M ∙ 0'20 1
TiempoEjecución = (M + ) ∙ (1 + 0' 2) ∙ ( ' ) = (0'82 ∙ 10-9 ) ∙ M segundos
8 1 5 ∙ 109

P1. Hallar la relación de CPI entre Pentium III y el Pentium IV para SPEC INT2000, con estos datos.

Pentium III Pentium IV P.IV / P.III


Número de Fases 11 22 2
Frecuencia para la misma tecnología 1’5 GHz 2’5 GHz 1’667 GHz
1’26 (1’7 GHz)
Prestaciones relativas SPEC INT2000 1 (1 GHz) Hallar relación
1’45 (2 GHz)
1’85 (1’5 GHz)
Prestaciones relativas SPEC FP2000 1 (1 GHz) Hallar relación
2’22 (2 GHz)

Solución:

Tomemos por ejemplo una de las referencias del Pentium IV de 1’26 (1’7 GHz). Donde se nos indica que
existe una aceleración de 1’26, teniendo en cuenta que referenciamos la del Pentium III como 1, luego:

RendimientoP.IV TiempoEjecución NºIns.P.III ∙ CPI.P.III ∙ PeriodoP.III


P.III
1' 26 = = =
RendimientoP.III TiempoEjecución NºIns.P.IV ∙ CPI.P.IV ∙ PeriodoP.IV
P.IV

Por tanto,
1
' CPI.P.III ⁄1'7∙109 CPI.P.III CPI.P.IV
1 26 = ∙ → = 0'7411 → = 1'3492
CPI.P.IV 1⁄ CPI.P.IV CPI.P.III
1'0∙109

245
P2. Dado el siguiente código de alto nivel:
int i;
float x[100], y[100], z[100];
for (i=0; i<100; i++)
x[i] = y[i] / z[i];

Suponer que la división tarda 10 ciclos y el resto de operaciones sólo 1 ciclo. Suponer BTB que siempre acierta.

a) Escribir el código (para un RISC escalar). Por comodidad usar un único registro base, modificando los
desplazamientos según el array.

b) Aplicar el Algoritmo de Tomasulo para un RISC escalar y calcular el CPI para el estacionario. Estudiar
los casos con Unidades Funcionales DIV tanto segmentada como no segmentada.

c) Ídem para superescalar de grado 4 (donde siempre puede lanzar 4 instrucciones en la fase IF, es decir
hay una IFU que tiene siempre llena la ventana de IF)

Solución:

Recordemos que una variable de tipo float requiere de 4 Bytes para su almacenamiento, luego los tres vectores
indicados tendrán la siguiente disposición en memoria:
x[0] x[99] y[0] y[99] z[0] z[99]

← 400 Bytes → ← 400 Bytes → ← 400 Bytes →

Apartado a)
bucle: LF FY, (Rb)400
LF FZ, (Rb)800
DIVF FX, FY, FZ
SF (Rb)0, FX
- - - - - - - -
ADDI Rb, Rb, 4 Dirección de &x[i]
SLT Rc, Rb, RfinX
BENZ Rc, bucle Dirección de &x[100] ∄ = &y[0]

246
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Aparatado b)

 Caso Escalar. Tendremos en cuenta el Algoritmo de Tomasulo para la regla WB → EX debido a las
dependencias reales.

1 LF FY, (Rb)400 IF IS EX WB FY
2 LF FZ, (Rb)800 IF IS EX WB FZ
3 DIVF FX, FY, FZ IF IS ○ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 WB FX
4 SF (Rb)0, FX IF IS ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB
5 ADDI Rb, Rb, 4 IF IS EX WB Rb
6 SLT Rc, Rb, RfinX IF IS ○ EX WB Rc
7 BENZ Rc, bucle IF IS ○ ○ EX WB

 Caso Unidad Funcional DIV sí segmentada.

LF FY, (Rb)400 IF IS EX WB FY

LF FZ, (Rb)800 IF IS EX WB FZ

DIVF FX, FY, FZ IF IS ○ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 WB FX

SF (Rb)0, FX IF IS ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB
ADDI Rb, Rb, 4 IF IS EX WB Rb

SLT Rc, Rb, RfinX IF IS ○ EX WB Rc

BENZ Rc, bucle IF IS ○ ○ EX WB


LF FY, (Rb)400 IF IS EX WB FY

LF FZ, (Rb)800 IF IS EX WB FZ

DIVF FX, FY, FZ IF IS ○ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 WB

 Caso Unidad Funcional DIV no segmentada.

LF FY, (Rb)400 IF IS EX WB FY

LF FZ, (Rb)800 IF IS EX WB FZ

DIVF FX, FY, FZ IF IS ○ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 WB FX

SF (Rb)0, FX IF IS ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB
ADDI Rb, Rb, 4 IF IS EX WB Rb

SLT Rc, Rb, RfinX IF IS ○ EX WB Rc

BENZ Rc, bucle IF IS ○ ○ EX WB


LF FY, (Rb)400 IF IS EX WB FY

LF FZ, (Rb)800 IF IS EX WB FZ

DIVF FX, FY, FZ IF IS ○ ○ ○ ○ D1 D2 D3 D4 D5 D6 D7 D8

10 ciclos ciclos
CPIestacionario = = 1' 43
7 instrucciones instrucción

247
Aparatado c) Superescalar grado 4.

 Caso Unidad Funcional DIV sí segmentada.

LF FY, (Rb)400 IF IS EX WB FY
FZ
LF FZ, (Rb)800 IF IS EX WB
DIVF FX, FY, FZ IF IS ○ ○ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 WB FX

SF (Rb)0, FX IF IS ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB
ADDI Rb, Rb, 4 IF IS EX WB Rb

SLT Rc, Rb, RfinX IF IS ○ ○ EX WB Rc

BENZ Rc, bucle IF IS ○ ○ ○ ○ EX WB


LF FY, (Rb)400 IF IS ○ ○ EX WB FY
FZ
LF FZ, (Rb)800 IF IS ○ EX WB
DIVF FX, FY, FZ IF IS ○ ○ ○ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 WB

 Caso Unidad Funcional DIV no segmentada.

LF FY, (Rb)400 IF IS EX WB FY
FZ
LF FZ, (Rb)800 IF IS EX WB
DIVF FX, FY, FZ IF IS ○ ○ D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 WB FX

SF (Rb)0, FX IF IS ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ EX WB
ADDI Rb, Rb, 4 IF IS EX WB Rb

SLT Rc, Rb, RfinX IF IS ○ ○ EX WB Rc

BENZ Rc, bucle IF IS ○ ○ ○ ○ EX WB


LF FY, (Rb)400 IF IS ○ ○ EX WB FY
FZ
LF FZ, (Rb)800 IF IS ○ EX WB
DIVF FX, FY, FZ IF IS ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ D1 D2 D3 D4 D5 D6 D7 D8 D9

1 ciclo
CPIIdeal =
4 instrucciones

10 ciclos
CPIEstacionario =
7 instrucciones

10 ciclos 1 ciclo ciclos


CPIBloqueo = CPIReal - CPIIdeal = - = 1'18
7 instrucciones 4 instrucciones instrucción

248
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P3. Traducir a un RISC típico usando el mínimo número de instrucciones posibles (un único registro base para
todos los vectores) el siguiente bucle.
float a[1024], b[1024];
for (i=2; i<1024; i++)
a[i] = (a[i] + a[i-2]) / b[i-1];

A continuación, reordenar, para que el mayor número de instrucciones posible esté detrás de DIVF.

Solución:

La traducción originará el siguiente esquemático código:


LF ai
LF ai2 Se aplica planificación estática, propio de los procesadores VLIW o
ADDF superescalares, que no implementan el Algoritmo de Tomasulo.
LF bi1
DIVF Observamos que, como existen dependencias entre todas las
SF ai
instrucciones útiles, sólo podemos reordenar las no útiles,
- - - - - - -
ADDI correspondientes a las del overhead: ADDI y SLTI respectivamente
SLTI (BNEZ no se puede reordenar).
BENZ

249
P4. Para una única iteración y pensando en el data flow limit (suponemos que se puede ejecutar en paralelo
todas las operaciones posibles; es decir, que no tengan dependencia), ¿cuántos ciclos tarda en almacenarse
a[i] del problema P3 desde que se empieza a leer el primer elemento de la memoria de esa iteración
(que sería a[i], a[i-2] o b[i])? Duración operaciones: ADDF: 4 ciclos, DIVF: 20 ciclos, LF: 3 ciclos, SF: 2 ciclos.

Solución:

# Ciclos

1 3 LF LF LF +
2 4 + COMP
F
3 20 ÷ BENZ

4 2 SF

29 ciclos (en una iteración suelta)

1 iteración ciclos
CPIIdeal = = 0'138
29 ciclos instrucción
4 instrucciones

Ahora cabe preguntarnos si es paralelizable o no lo es. El código sería el siguiente:


float a[1024], b[1024];
for (i=2; i<1024; i++)
{
a[i ] = (a[i ] + a[i-2]) / b[i-1];
a[i+1] = (a[i+1] + a[i-1]) / b[i ];
a[i+2] = (a[i+2] + a[i ]) / b[i+1];
}

Observamos que existe una recurrencia, por lo que el bucle no es paralelizable.

La recurrencia encontrada es: ai = f(ai-1) ai+2 = f(ai)

250
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P5. Suponiendo que CPI ideal = 1, CPI datos = 0’15 y CPI control = (Frecuencia de saltos) * 1’5 ciclos bloqueo/salto, hallar
el número de instrucciones del bucle SAXPY original (para un total de 1024*3 iteraciones) y del desenrollado
de 3 iteraciones. Hallar también la aceleración entre ambos bucles.

Solución:

El código ensamblador general de un bucle SAXPY es el siguiente:


1 LF FX, (Rx)
2 LF FY, (Ry)
3 MULTF Fm, Ra, FX
4 ADDF FY, FY, Fm
5 SF (Rx), FY
- - - - - - - - - -
6 ADDI Rx, Rx, 4
7 ADDI Ry, Ry, 4
8 SLTI Rc, Rx, RtamX
9 BENZ Rc, Bucle

Sabiendo que:

𝐶𝑃𝐼𝑆𝐴𝑋𝑃𝑌 = 𝐶𝑃𝐼𝐼𝑑𝑒𝑎𝑙 + 𝐶𝑃𝐼𝑑𝑎𝑡𝑜𝑠 + 𝐶𝑃𝐼𝑐𝑜𝑛𝑡𝑟𝑜𝑙

entonces tenemos:

Número de instrucciones sin desenrollar: (5 útiles ∙ 1 iteración) + 4 overhead = 9 instrucciones


1 Ciclo CiclosBloqueo 79 ciclos
CPI1 = (1) + (0'15) + ( ∙ 1' 5 ) =
9 Instrucciones Salto 60 instrucción

Número de instrucciones sin desenrollar: (5 útiles ∙ 3 iteraciones) + 4 overhead = 19 instrucciones


1 Ciclo CiclosBloqueo 467 ciclos
CPI3 = (1) + (0'15) + ( ∙ 1' 5 ) =
19 Instrucciones Salto 380 instrucción

Por tanto, obtenemos la aceleración a partir de la siguiente expresión:


79
(9 ∙ (1024∙3 iteraciones)) ∙
Aceleración =
Instrucciones1 ∙ CPI1 ∙ T1
= 60 ∙ T1 = 0'51
Instrucciones3 ∙ CPI3 ∙ T3 467
(19 ∙ (1024∙3 iteraciones)) ∙ ∙T
380 3
De donde se deduce que el código desenrollado de SAXPY3 es un 51% más rápido respecto a SAXPY1.

251
P6. Hallar el ancho de banda de acceso al sistema de memoria (AB MEM) para los dos casos del problema
anterior, si la frecuencia = 2 GHz. Calcule también los GFLOPS.

Solución:

En el bucle SAXPY anterior contamos con 3 instrucciones de acceso a memoria: LF, LF y SF que requieren 4 Bytes
cada una de ellas. El ancho de banda de acceso a memoria requerido para cada caso es:

Sabiendo que:

(InstruccionesAcceso ∙ Tamañodato ) ∙ Iteraciones


ABMEM =
(InstruccionesTotales ∙ Iteraciones) ∙ CPI ∙ Periodo

entonces tenemos:

(3 {LF,LF,SF} ∙ 4 Bytes) ∙ (1024∙3) Bytes GBytes


ABMEM1 = = 2.025.316.456 ≡ 1'89
79 ciclos 1 segundo segundo segundo
(9 ∙ (1024∙3) instrucc.) ∙ ∙
60 instrucc. 2∙109 ciclos

(3 {LF,LF,SF} ∙ 4 Bytes) ∙ (1024∙3) Bytes GBytes


ABMEM3 = = 1.027.837.259 ≡ 0'96
467 ciclos 1 segundo segundo segundo
(19 ∙ (1024∙3) instrucc.) ∙ ∙
380 instrucc. 2∙109 ciclos

Sabiendo que los GFLOPS [Giga FLoat OPerators Per Second] referencian a los operadores: ADDF, SUBF, MULTF y
DIVF, por unidad de tiempo; y que su cálculo viene determinado por la siguiente expresión:

InstruccionesFLOP ∙ Iteraciones Iteraciones


GFLOPS = ABMEM ∙ ∙
Iteraciones (InstruccionesAcceso ∙ Tamañodato ) ∙ Iteraciones

entonces tenemos:

GBytes 2 {MULTF,ADDF} FLOP ∙ (1024∙3) (1024∙3) elementos procesados GFLOP


GFLPOS1 = 1'89 ∙ ∙ = 0'31
( ) ( ) (
segundo 1024∙3 elementos procesados 3 {LF,LF,SF} ∙ 4 Bytes ∙ 1024∙3) segundo

GBytes 2 {MULTF,ADDF} FLOP ∙ (1024∙3) (1024∙3) elementos procesados GFLOP


GFLPOS3 = 0'96 ∙ ∙ = 0'16
segundo (1024∙3) elementos procesados (3 {LF,LF,SF} ∙ 4 Bytes) ∙ (1024∙3) segundo

252
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P7. Desenrollar usando 2 variables con mínimos parciales el bucle que calcule el mínimo elemento de un
vector. Tras el bucle el mínimo sería el mínimo de ambas variables.

Solución:
index_min0 = 0;
min0 = v[0];

for (i=1; i<N; i++)


if (min0 > v[i]) {
index_min0 = i;
min0 = v[i];
}

for (i=2; i<N; i+=2) {


if (aux0 > v[i+0]) {
index_min0 = i + 0;
aux0 = v[index_min0];
}
if (aux1 > v[i+1]) {
index_min1 = i + 1;
aux1 = v[index_min1];
}
}

if (aux1 < aux0) {


index_minimum = index_min1;
minimum = aux1;
} else {
index_minimum = index_min0;
minimum = aux0;
}

253
254
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 6 – Programación de computadores usando OpenMP.

1. Objetivos y preparación.

En la actualidad los multiprocesadores en un solo chip (denominados habitualmente procesadores de múltiple


núcleo) son los predominantes en el mercado. Si queremos aprovechar las posibilidades de estos procesadores
es necesario particionar nuestro código en diversos hilos de forma que puedan ejecutarse simultáneamente
tanto hilos como procesadores tengamos. Con el fin de facilitar este proceso se han introducido herramientas
que permiten realizar esta partición de manera sencilla sin tenerse que preocupar de crear y destruir los hilos o
de gestionar las comunicaciones entre ellos de forma explícita. Dentro de estas herramientas una de las más
populares es el entorno openMP que hoy en día está soportado en la mayoría de multiprocesadores con
memoria compartida. En la familia 80x86 openMP están soportados los compiladores de C++ de Microsoft e Intel
(Windows y Linux) y otros. La referencia de openMP podemos encontrarla en http://openmp.org/wp/.

OpenMP utiliza para generar el código paralelo directivas y funciones insertadas en el código C++ (o Fortran). En
C las directivas usan el formato:
#pragma omp directiva [comando[(parámetros)], [comando[(parámetros)]

Tanto las directivas como las funciones no son tenidas en cuenta si:

 No habilitamos la compatibilidad openMP en el compilador. En Visual Studio la opción para habilitar está
compatibilidad se encuentra en: Propiedades de Configuración → C/C++ → Idioma → Compatibilidad
con openMP.
 Y se inserta #include <omp.h>

2. Realización de la práctica.

EJERCICIO 1.

Abre la solución p6. Dentro de ella elige el proyecto ej1 para que sea el proyecto inicial y abre dentro de él, el
fichero principal_ej1.cpp, que contiene este código:
using namespace std;
#include <iostream>
#include <omp.h>
int main()
{
cout << "Hola, mundo!" << endl;
}

a) Lo más sencillo que se le puede pedir a OpenMP es que cree diferentes hilos y ejecute en todos ellos un mismo
código. Cambia la línea cout … en main() por esta otra:
int main()
{
#pragma omp parallel
cout << "Hola, mundo!" << endl;
}

Si el sistema en el que se está ejecutando este programa contiene dos o más procesadores (lógicos o físicos), se
habrán creado tantos hilos de ejecución paralela como procesadores existan, así que deberían aparecer tantos
mensajes “Hola, mundo!” como hilos se hayan creado. Si sólo aparece un mensaje, y nuestro sistema es
multicore, probablemente signifique que nuestro compilador no soporta directivas OpenMP, o bien que éstas no
han sido habilitadas (ver punto 1 de la práctica)
255
b) Se puede forzar un número de hilos determinado usando la siguiente función del API de OpenMP:
omp_set_num_threads (numhilos); Cambia el código para crear 4, 8 o 16 hilos.

using namespace std;


#include <iostream>
#include <omp.h>
int main()
{
omp_set_num_threads(4); // crea 4 hilos para cada pragma omp parallel que se encuentre
#pragma omp parallel
{
cout << "Hola, mundo!" << endl;
}
}

Tanto en este caso como en el anterior, es más que posible que los mensajes en pantalla aparezcan “pisados”
unos con otros. Esto es porque las múltiples llamadas a cout se ejecutan a la vez. Lo solucionaremos en un
instante.

c) Habitualmente nos interesa que cada uno de los hilos que se crea sea consciente de que es un hilo dentro de
un conjunto de hilos que se ejecuta en paralelo. Para ello hay dos funciones del API de OpenMP:

int omp_get_thread_num() : Esta función devuelve el número de hilo en el que se encuentra.


int omp_get_num_threads() : Esta función devuelve el número de hilos que se están ejecutando en paralelo en
este momento.

Cambia el código para que el mensaje que se muestra por pantalla indique qué hilo se está ejecutando y cuántos
hay. Es decir, que aparezcan líneas con un texto tal como: Hola, mundo! Soy el hilo __ de __

d) Si dentro de los hilos hay una cierta sección del código que no es reentrante, como puede ser la escritura en
la consola, podemos usar cerrojos (locks) para crear secciones críticas, de forma que nos aseguremos de que
sólo un hilo está ejecutando esa región. OpenMP dispone de una API para manejar cerrojos de forma sencilla.
Consiste en los siguientes tipos y funciones:

- omp_lock_t : Para cada cerrojo que necesitemos, declararemos una variable que sea de este tipo. Por
ejemplo: omp_lock_t cerrojo;
- omp_init_lock (omp_lock_t *lock); Se llamará a esta función indicando la dirección de una variable de tipo
omp_lock_t para inicializar ese cerrojo. Los cerrojos se inicializan al estado “disponible”. Esta operación se
hace sólo una vez durante la vida útil del cerrojo y fuera de los hilos en paralelo.
- omp_set_lock (omp_lock_t *lock); Cuando uno de los hilos necesite entrar en una región crítica, llamará a
esta función. Si el cerrojo está disponible, la función termina sin esperar, y lo marca como no disponible. Si
el cerrojo no estuviera disponible, el hilo se queda esperando hasta que lo esté.
- omp_unset_lock (omp_lock_t *lock); Cuando un hilo termine su región crítica, llamará a esta función para
marcar el cerrojo como disponible. De esa forma otro hilo tendrá la oportunidad de entrar en la región crítica.
- omp_destroy_lock (omp_lock_t *lock); Se llamará a esta función la dirección de una variable de tipo
omp_lock_t para destruir e invalidar ese cerrojo. Una vez destruido no puede volver a usarse salvo que se
vuelva a inicializar. Esta operación se realizará fuera de los hilos en paralelo.

Cambia el código para que cree 16 hilos de ejecución, y usa un cerrojo para encerrar en una región crítica la
ejecución de la orden cout para imprimir en pantalla. Comprueba que aparecen los 16 mensajes
correspondientes, sin pisarse unos a otros.

e) Busca información sobre la directiva #pragma critical y úsala para simplificar el programa obtenido en d)
256
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Solución:

using namespace std;

#include <iostream>
#include <omp.h>

int main(){
omp_set_num_threads(8);
omp_lock_t cerrojo;
omp_init_lock(&cerrojo);
#pragma omp parallel
{
int numThreads = omp_get_num_threads();
int mythreads = omp_get_thread_num();
omp_set_lock(&cerrojo);
cout << "Soy el hilo " << mythreads << " de " << numThreads << endl;
omp_unset_lock(&cerrojo);

}
omp_destroy_lock(&cerrojo);
}

EJERCICIO 2.

Ahora vamos a usar el proyecto ej2, en la misma Solución. Activa ej2 y márcalo como proyecto de inicio. Abre
dentro de él, el fichero principal_ej2.cpp . En él hay dos funciones, que usaremos, la primera para los apartados
a) b) y c); y la segunda para el apartado d). Comenta la que no vayas a usar, y descomenta la que sí.

a) Queremos paralelizar este bucle. Es decir, que varias iteraciones del mismo se ejecuten en diferentes hilos.
Para ello se ha añadido una directiva #pragma omp parallel de forma que el bloque que contiene el bucle
se ejecute concurrentemente en varios hilos. Queremos que se ejecute el mismo número de iteraciones,
pero cada una de ellas (o un grupo de ellas) en hilos diferentes. Prueba primero el programa comentando el
#pragma y comprueba que el número de iteraciones que produce es el esperado. Descomenta el #pragma
para que surta efecto ¿Qué resultado tenemos ahora? (ignora el que los resultados en pantalla se pisen, o
mejor aún, usa cerrojos o #pragma omp critical para evitar que esto se produzca).

Solución:
void part_a_b_c (int n)
{
#pragma omp parallel
{
int this_thread = omp_get_thread_num();
for (int i=0; i<n; i++)
{
#pragma omp critical
cout << "La iteracion " << i << " la gestiona el hilo " << this_thread << endl;
}
}
}

257
b) Usa las funciones omp_get_thread_num(); y omp_get_num_threads(); para particionar el bucle de la
siguiente forma: el bucle da N vueltas. Hay T hilos, donde T es el valor devuelto por
omp_get_num_threads();. Supondremos que el número de vueltas N es divisible entre el número de hilos T,
o sea, N % T = 0. Entonces, para cada hilo t (0 ≤ t < T), el valor inicial del bucle for será t*(N/T) y el valor final
(al cual no debe llegar el bucle), (t+1)*(N/T). Comprueba que el número de iteraciones es el correcto tanto
si el #pragma existe como si no.

Solución:

void part_a_b_c (int n)


{
#pragma omp parallel
{
int this_thread = omp_get_thread_num();
int total_threads = omp_get_num_threads();
int desde = this_thread * (n/total_threads);
int hasta = (this_thread+1) * (n/total_threads);
for (int i=0; i<n; i++)
{
#pragma omp critical
cout << "La iteracion " << i << " la gestiona el hilo " << this_thread << endl;
}
}
}

c) Usando #pragma omp parallel for simplifica el programa del apartado b).

Solución:
void part_a_b_c (int n)
{
omp_set_num_threads(4);
#pragma omp parallel for
for (int i=0;i<n;i++)
{
int this_thread = omp_get_thread_num();
#pragma omp critical
cout << "La iteracion " << i << " la gestiona el hilo " << this_thread << endl;
}
}

d) El bucle mostrado en esta función calcula la suma de los N primeros términos de 0+1+2+3+…+(N-1).
Aplicando los #pragma adecuados, distribuye el cálculo de la suma en distintos hilos. ¿Qué resultados se
observan? ¿Cómo se puede arreglar?

Solución:

void part_d (unsigned n)


{
signed suma=0;
omp_set_num_threads(8);
#pragma omp parallel for reduction (+:suma)
for (int i=0; i<n; i++)
{
suma = suma + i;
}
cout << "Resultado : " << suma << endl << "Prueba: " << " " << ((n-1)*n)/2 << endl;
}

258
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

EJERCICIO 3.

Activa el proyecto ej3, márcalo como proyecto de inicio y dentro de él, abre el fichero principal_ej3.cpp. Este
programa cronometra el tiempo de ejecución de la función que se ponga en el sitio indicado. Se pide escribir
una serie de funciones usando las directivas de OpenMP, y cronometrarlas. Para cada función hay que
cronometrar su tiempo de ejecución dos veces: una comentando todos los #pragma omp que haya insertado en
la función, y otra con todas las directivas activadas. Con los datos obtenidos, rellene la tabla de la última página
de este boletín.

Las funciones son:

 void saxpy (int n) Solución:


Calcula Y = a*x + Y, donde el escalar a, y void saxpy (int n)
los vectores X, Y de n elementos, están ya {
definidos como variables globales. int i;
#pragma omp parallel for
for(i=0; i<n; i++)
{
y[i] = a * x[i] + y[i];
}
}

 float dotproduct (int n) Solución:


Calcula y devuelve el producto escalar de float dotproduct (int n)
los vectores de n elementos X e Y, ya {
definidos. El producto escalar de dos float suma = 0.0;
int i;
vectores se calcula como: #pragma omp parallel for reduction (+:suma)
𝑛−1 for(i=0; i<n; i++)
{
𝑋⃑ ∙ 𝑌
⃗⃑ = ∑(𝑋𝑖 ∙ 𝑌𝑖 ) suma = suma + x[i] * y[i];
𝑖=0 }
return suma;
En la implementación de esta función }
debe usarse una variable acumulador.

 int countzeros (int n) Solución:


Cuenta y devuelve el número de int countzeros (int n)
elementos del vector Z que tienen valor {
0. El vector Z ya ha sido declarado e int count = 0;
int i;
inicializado como variable global con un #pragma omp parallel for reduction (+:count)
total de n elementos. En la for(i=0; i<n; i++)
implementación de esta función debe {
if (z[i] == 0)
usarse una sentencia if dentro del count++;
cuerpo del bucle. }
return count;
}

259
RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 6. OPENMP.

ALUMNO:

SAXPY DOTPRODUCT COUNTZEROS


N T. sin T. con T. sin T. con T. sin T. con
SpeedUp SpeedUp SpeedUp
OMP OMP OMP OMP OMP OMP
64 1,218750 33,96875 0,0358785649 1,812500 46,21875 0,0392156863 2,078125 46,109375 0,0450694680
128 1,367188 16,17969 0,0845002697 2,085938 22,91406 0,0910330918 2,265625 24,601563 0,0920927260
256 1,371094 8,66797 0,1581793844 2,359375 11,46875 0,2057220708 2,335938 13,476563 0,1733333640
512 1,380859 5,26758 0,2621430570 2,355469 6,24414 0,3772286692 2,351563 6,173828 0,3808922114
1024 1,374023 2,30176 0,5969450307 2,356445 3,42578 0,6878562874 2,467773 3,265625 0,7556816842
2048 1,374512 1,38281 0,9939970191 2,358398 2,10742 1,1190914776 3,259766 2,154785 1,5128033655
4096 1,378906 0,93823 1,4696855362 2,360107 1,42285 1,6587157343 6,068848 2,216900 2,7375380035
8192 1,383789 0,65064 2,1268284061 2,358276 1,11462 2,1157592157 8,342651 2,314575 3,6043986477
16384 1,290344 0,52637 2,4514150773 2,357910 0,81165 2,9050965569 9,292053 2,316162 4,0118320739
32768 1,251373 0,46115 2,7135862223 2,129639 0,67688 3,1462578300 8,988464 2,368400 3,7951629792
65536 1,281143 0,42969 2,9815656942 2,131348 0,63481 3,3574528482 8,955261 2,386627 3,7522666927
131072 1,278770 0,41758 3,0623353609 2,129562 0,61426 3,4668852502 9,011307 2,440323 3,6926697818
262144 1,275684 0,41011 3,1105898417 2,129730 0,60208 3,5372815286 9,063919 2,495209 3,6325289785
524288 1,323477 0,82395 1,6062510771 2,168659 0,64000 3,3885296875 9,078442 2,515265 3,6093381811
1048576 1,944133 1,56341 1,2435177119 2,352974 1,07155 2,1958683995 9,085072 2,518845 3,6068404368
2097152 2,318551 2,09917 1,1045063439 2,391600 1,30299 1,8354693164 9,083265 2,517696 3,6077687695
4194304 2,310124 2,11413 1,0927061757 2,391981 1,28840 1,8565587417 9,126407 2,518580 3,6236319672
8388608 2,306789 2,13817 1,0788628765 2,393762 1,31390 1,8218808795 9,164628 2,529290 3,6233994520

Nota: Como podemos observar, en los


cambios de caché de L1 a L2, y de L2 a
L3, y de L3 a memoria principal existen
pequeños saltos donde se penaliza en
tiempo de ejecución por acceso a un
nivel de memoria más lento.

¿Es el speedup uniforme para todos los


valores de N? Respuesta: No.

¿A qué se puede achacar las discrepancias que hay en los extremos?

Para valores de N muy pequeños se da una mayor velocidad sin OpenMP ya que se reducen los fallos por caché
al sólo haber un procesador y una caché asociada a dicho procesador. Además no existe código de sincronización
de hilos. Para valores de N muy grandes, con OpenMP se empieza a mejorar y para N enormes la mejora es
visible pero no excesiva, ya que se producen los mismos fallos de cachés que se producían para un sólo
procesador. Por lo general, en las versiones con OpenMP se producen los fallos de caché de la versión sin
OpenMP a partir de un CHUNK asignado grande; es decir, cuando a cada procesador se le asigna gran tamaño
de vector; de forma que la misma línea de caché sólo estará en un hilo a la vez y no se produzca ningún "tránsito"
de líneas de caché por "husmeo", que es lo que se produce en las ejecuciones con OpenMP en tamaños
pequeños (la misma línea se usa en cachés distintas y hay que sincronizar dichos datos).

260
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Para una misma máquina y un mismo valor de N, ¿es el speedup conseguido el mismo para los tres algoritmos?
¿A qué se puede achacar las diferencias de speedup entre cada uno?

Respuesta: No. La principal diferencia radica en la paralelización de cada uno. El primero (SAXPY) va a ser
totalmente vectorizable pero se va a escribir con dependencias en la misma i que se lee. En el segundo y en el
tercero existe una variable que debe ser "compartida" y ha de respetarse de alguna forma. En el producto escalar
se forma parte de la propia operación, pero hay una parte vectorizada totalmente [los vectores x e y] y en la
cuenta de ZEROS se comparte la variable contador mediante el reduction y la parte vectorizada sólo incluye un
vector. En el algoritmo de ZEROS sólo se usa la caché para un vector, por ello sus "fallos de caché" se darán más
tarde. De ahí su mejor rendimiento. La mejora es menor en el SAXPY motivado por el triple acceso a memoria
(lectura de x[i], lectura de y[i] y escritura en y[i]), más la sincronización que ello debe conllevar.

¿Es el speedup uniforme para todos los valores de N? ¿A qué se


puede achacar las discrepancias que hay en los extremos?
(para N muy pequeña o N muy grande).

Casos SAXPY y DotProduct: Los valores tan altos que se


observan al inicio se debe a que debe crear los hilos y esto
supone un alto coste computacional, que se compensa
conforme van aumentando los valores de N. El incremento del
speedup para valores N muy grandes se debe a los fallos de
caché; esto es, ahora debe acceder a RAM, lo que vuelve a
suponer un alto coste computacional.

Caso CountZeros: El speedup se mantiene invariable debido a


que dicho algoritmo no accede a elementos vectoriales, sino
que se realiza únicamente operaciones aritméticas.

Para una misma máquina y un mismo valor de N, ¿es el speedup


conseguido el mismo para los tres algoritmos? ¿A qué se puede
achacar las diferencias de speedup entre cada uno?

Caso SAXPY: Se consigue una aceleración menor porque hay


que realizar muchas operaciones con los elementos de los
vectores, incluyendo su correspondiente acceso a caché.
Además hay que tener cuidado con la dependencia de la
iteración iésima por el vector.

Caso DotProduct: Es algo más rápido porque no hay tantas


operaciones, la única operación a realizar es sencilla.

Caso CountZeros: Es más rápida porque es aún más sencilla,


sólo hay que comprobar un elemento del vector por cada
iteración y sumar 1 al acumulador.

261
262
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 7 – Programación de computadores usando MPI.

Preparación de la práctica.

Objetivos.

En esta práctica se pretende que el alumno conozca la metodología a emplear para desarrollar aplicaciones
paralelas para computadores paralelos de memoria distribuida y se familiarice con las particularidades que
comporta este tipo de plataforma distribuida para la ejecución de los programas, su depuración y su rendimiento.

Metodología.

Se estudiarán los fundamentos de la programación utilizando el paradigma más empleado hoy en día en este
tipo de plataforma: la biblioteca de funciones MPI (Message-Passing Interface), junto con el lenguaje de
programación C.

Se realizarán programas para un PC con sistema operativo Linux, usando el compilador gcc junto con la biblioteca
de funciones MPI. El alumno empleará un único PC para aprender los mecanismos de desarrollo y ejecución de
programas paralelos que se emplean en plataformas distribuidas tales como un clúster. La ejecución en un PC de
la aplicación con distinto número de procesos podrá beneficiarse de los múltiples núcleos del procesador.

Estudio Previo.

Estudiar los contenidos relacionados con la programación paralela de multiprocesadores con memoria
distribuida impartidos en teoría.

Leer atentamente la presentación de diapositivas titulada "Introducción a la programación de computadores de


memoria distribuida usando MPI" que forma parte del material de esta práctica.

Ambos contenidos podrán ser evaluados en un test de conocimientos previos al comienzo de esta práctica.

Realización en Laboratorio.

Como documentación básica para trabajar usando el sistema operativo Linux, el alumno puede emplear el
documento “Comandos UNIX” escrito por el Centro de Cálculo de la E.T.S.I.I., que se encuentra en la URL:
http://www.informatica.us.es/index.php/en/servicios/centro-de-calculo/comandos-unix

Tras iniciar sesión en Linux, abrir una consola de terminal y crear un directorio de trabajo para esta práctica,
llamado practica-mpi. Crear o editar los archivos de código fuente empleando cualquier editor de texto de Linux
(por ejemplo “gedit” o “nano”). Los comandos para compilar y ejecutar programas que usan MPI son los
siguientes:

Compilación: mpicc.openmpi nombre_archivo.c -o nombre_archivo [-lm]

El modificador opcional -lm hace que el compilador enlace el programa con las librerías matemáticas.

Ejecución: mpirun -np X nombre_archivo

Donde X es un número que indica el número de procesos que debe lanzar la aplicación.
263
EJERCICIO 1

Modificar el programa “hola_mundo_avanzado.c” (presentado en las transparencias incluidas en el material de


esta práctica) para que en el mensaje de salida que muestra por pantalla cada proceso incluya al final el nombre
del procesador en que está corriendo ese proceso. Por ejemplo, una salida típica podría ser:
[practica@hal practica-mpi]$ mpirun -np 3 hola_mundo_avanzado2
Soy el proceso 0 de 3 corriendo en hal: !Hola mundo!
Yo soy el proceso 1 de 3, corriendo en hal.
Yo soy el proceso 2 de 3, corriendo en hal.

Pista: Utilizar la función MPI_Get_processor_name.

Investigar si se puede lanzar un número de procesos que sea mayor que el número de núcleos del procesador
utilizado y qué ocurre en ese caso.

EJERCICIO 2:

Crear un archivo de código fuente C conteniendo el programa “saludos.c” (presentado en las transparencias
incluidas en el material de esta práctica), compilarlo y ejecutarlo sobre distinto número de procesadores. ¿Qué
ocurre al ejecutar el programa sólo para un procesador? ¿Por qué?

EJERCICIO 3:

Partiendo del programa “saludos.c”, crear un programa llamado “saludos_en_anillo.c” en el cual el proceso i
le envía un saludo al proceso (i+1)%p (donde el operador % es el módulo matemático o resto de la división
entera). Prestar atención a cómo el proceso i calcula de quién debe recibir un mensaje. Contestar además:

a) El proceso i, ¿debe enviar primero su mensaje al proceso i+1 y después recibir el mensaje del proceso i-
1, o a la inversa (primero recibir y luego enviar)? ¿Importa el orden?
b) ¿Qué ocurre cuando se ejecuta el programa sobre un único procesador (con un único proceso)?

EJERCICIO 4:

A continuación se proporciona un programa "prod_escalar_mpi.c" que es un ejemplo de cálculo simple en


paralelo que realiza el producto escalar de dos vectores.

- Estudiar el funcionamiento del programa. Para ello, cambiar inicialmente el número de elementos de los
vectores a un valor muy pequeño (por ejemplo 6). Ejecutar el programa variando el número de procesos
lanzados desde 1 hasta 3, comprobar que los resultados son correctos y estudiar el código para entender
cómo se realiza el cálculo.

- Modificar el programa añadiéndole una medición del tiempo de ejecución, usando para ello la función
MPI_Wtime.

- Medir el tiempo de ejecución del programa al variar el número de procesos con el que es lanzado desde
1 hasta 6 procesos.

¿Son lógicos los tiempos de ejecución que se observan?

264
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Programa prod_escalar_mpi.c:

/* prod_escalar_mpi.c
* CALCULO DEL PRODUCTO ESCALAR DE DOS VECTORES
* ENTRADA: NINGUNA.
* SALIDA : PRODUCTO ESCALAR
*/

#include <stdio.h>
#include <math.h>
#include <mpi.h>

#define ELEMENTOS (2*2*2*2*3*3*5*7*11*13) // vale 720720


//* ¡ATENCION: EL Nº DE ELEMENTOS DEBE SER DIVISIBLE POR EL Nº DE PROCESOS (p)! */

float x[ELEMENTOS], y[ELEMENTOS]; // VECTORES (VARIABLES GLOBALES)

float prod_escalar_serie(float a[], float b[], float n);

main(int argc, char** argv) {


int mi_rango; // RANGO DE MI PROCESO
int p; // NUMERO DE PROCESOS
int n = ELEMENTOS; // NUMERO DE ELEMENTOS DE LOS VECTORES
int n_local; // NUMERO DE ELEMENTOS DE CADA FRAGMENTO
int inicio_vector_local; // INDICE DE INICIO DE CADA FRAGMENTO
float suma_local; // PRODUCTO ESCALAR SOBRE MI INTERVALO
float suma_total; // PRODUCTO ESCALAR TOTAL
int fuente; // PROCESO QUE ENVIA RESULTADO DE SUMA
int dest = 0; // DESTINATARIO: TODOS LOS MENSAJES VAN A 0
int etiqueta = 0;
int i;
MPI_Status status;
MPI_Init(&argc, &argv); // INICIALIZA MPI
/* INICIALIZA LOS VECTORES */
for (i = 0; i < n; i++)
x[i] = y[i] = i%5;
MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango); // AVERIGUA EL RANGO DE MI PROCESO
MPI_Comm_size(MPI_COMM_WORLD, &p); // AVERIGUA CUANTOS PROCESOS HAY
n_local = n/p;
inicio_vector_local = mi_rango * n_local;
suma_local = prod_escalar_serie(&x[inicio_vector_local], &y[inicio_vector_local], n_local);
printf("MI RANGO = %d , SUMA LOCAL = %f\n", mi_rango, suma_local);
/* SUMA LAS CONTRIBUCIONES CALCULADAS POR CADA PROCESO */
if (mi_rango == 0) {
suma_total = suma_local;
for (fuente = 1; fuente < p; fuente++) {
MPI_Recv(&suma_local, 1, MPI_FLOAT, fuente, etiqueta, MPI_COMM_WORLD, &status);
suma_total = suma_total + suma_local;
}
} else {
MPI_Send(&suma_local, 1, MPI_FLOAT, dest, etiqueta, MPI_COMM_WORLD);
}
/* MUESTRA EL RESULTADO POR PANTALLA */
if (mi_rango == 0) {
printf("PRODUCTO ESCALAR USANDO p=%d TROZOS DE LOS VECTORES X E Y = %f\n", p, suma_total);
}
MPI_Finalize(); // CIERRA EL UNIVERSO MPI */
}

/* FUNCION QUE CALCULA EL PRODUCTO ESCALAR LOCAL (DE UN TROZO DE LOS VECTORES) */
float prod_escalar_serie(float a[], // ENTRADA
float b[], // ENTRADA
float n // ENTRADA: NUMERO DE ELEMENTOS
) {
int i;
float suma = 0.0;
for (i = 0; i < n; i++)
suma = suma + a[i] * b[i];
return suma;
}

265
TABLA DE RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 7. PROGRAMACIÓN DE COMPUTADORES DE MEMORIA DISTRIBUIDA USANDO MPI.

ALUMNO:

Ejercicio 1:

Código fuente del programa:


#include <stdio.h>
#include <mpi.h>

int main(int argc, char *argv[]) {

int mi_rango, tamano, len;


char host [MPI_MAX_PROCESSOR_NAME];

MPI_Init(&argc, &argv); /* Inicializa MPI */


MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango); /* id del proceso */
MPI_Comm_size(MPI_COMM_WORLD, &tamano); /* Número de procesos */
MPI_Get_processor_name(host, &len);

if (mi_rango == 0)
printf ("Soy el proceso %i de %i : Hola mundo\n", mi_rango, tamano);
else
printf ("Soy el proceso %d de %d, en %s.\n", mi_rango, tamano, host);

MPI_Finalize(); /* Cierra el universo MPI */


return(0);
}

Líneas adicionales añadidas int mi_rango, tamano, len;


char host [MPI_MAX_PROCESSOR_NAME];
al programa o líneas que se MPI_Init(&argc, &argv);
han modificado: MPI_Get_processor_name(host, &len);
printf("Soy el proceso %i de %i : Hola mundo\n", mi_rango, tamano);
printf("Soy el proceso %d de %d, en %s.\n", mi_rango, tamano, host);
¿Se puede lanzar un
número de procesos mayor
Sí se puede.
que el número de núcleos
del procesador utilizado?

¿Qué ocurre en ese caso? El sistema operativo conmuta los diferentes procesos entre los distintos núcleos.

266
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio 2:

Código fuente del programa:


#include <stdio.h>
#include <string.h>
#include <mpi.h>

int main(int argc, char* argv[])


{
int mi_rango; // rango de mi proceso
int p; // numero de procesos
int fuente; // rango del emisor
int destino; // rango del destinatario
int etiqueta = 0; // etiqueta para los mensajes
char mensaje[100]; // almacenamiento para el mensaje
MPI_Status status; // devuelve el status para el receptor

MPI_Init (&argc, &argv); // Inicializa MPI


MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango);
MPI_Comm_size(MPI_COMM_WORLD, &p);

if (mi_rango != 0)
{
/* Crea mensaje */
sprintf(mensaje, "Saludos desde el proceso %d!", mi_rango);
destino = 0;
/* Usa strlen+1 para que '\0' pueda ser transmitido */
MPI_Send(mensaje, strlen(mensaje)+1, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD);
}
else
{
// mi_rango == 0
for (fuente=1; fuente < p; fuente++)
{
MPI_Recv(mensaje, 100, MPI_CHAR, fuente, etiqueta, MPI_COMM_WORLD, &status);
printf("%s\n", mensaje);
}
}
MPI_Finalize(); // Cierra el universo MPI
} // main

¿Qué ocurre al ejecutar el programa "saludos" sólo para un procesador? ¿Por qué?

Al ejecutarse para un solo procesador el código que se ejecuta es el envío de un mensaje y no hay
quien lo reciba. Sin embargo, si se ejecutase para más de un procesador, la salida por pantalla
sería diferente, el código que se ejecuta es mostrar un saludo desde los diferentes porcesos que
reciben un mensaje desde ellos.

267
Ejercicio 3:

Código fuente del programa:


#include <stdio.h>
#include <string.h>
#include <mpi.h>

int main(int argc, char* argv[])


{
int mi_rango; // rango de mi proceso
int p; // numero de procesos
int fuente; // rango del emisor
int destino; // rango del destinatario
int etiqueta = 0; // etiqueta para los mensajes
char mensaje[100]; // almacenamiento para el mensaje
MPI_Status status; // devuelve el status para el receptor

MPI_Init (&argc, &argv); // Inicializa MPI


MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango);
MPI_Comm_size(MPI_COMM_WORLD, &p);

/* Crea mensaje */
sprintf(mensaje, "Saludos desde el proceso %d!", mi_rango);
destino = (mi_rango+1)%p;
/* Usa strlen+1 para que '\0' pueda ser transmitido */
MPI_Send(mensaje, strlen(mensaje)+1, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD);

fuente = (mi_rango-1+p)%p;
MPI_Recv(mensaje, 100, MPI_CHAR, fuente, etiqueta, MPI_COMM_WORLD, &status);
printf("%s\n", mensaje);

MPI_Finalize(); // Cierra el universo MPI


} // main

a) El proceso i, ¿debe enviar primero su mensaje al proceso i+1 y después recibir el mensaje del proceso i-1, o
a la inversa (primero recibir y luego enviar)? ¿Importa el orden?

Primero debe enviar el mensaje y luego esperar a recibirlo. El orden sí es importante, ya que no se podría
estar esperando un mensaje que aún no ha sido enviado.

b) ¿Qué ocurre cuando se ejecuta el programa sobre un único procesador (con un único proceso)?

Que el mismo procesador envía y recibe el mensaje.

268
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio 4:
01 /* prod_escalar_mpi.c
02 * CALCULO DEL PRODUCTO ESCALAR DE DOS VECTORES
03 *
04 * ENTRADA: NINGUNA.
05 * SALIDA : PRODUCTO ESCALAR
06 */
07 #include <stdio.h>
08 #include <math.h>
09 #include <mpi.h>
10 #define ELEMENTOS (2*3) // vale 6
11 /* ¡ATENCION: EL Nº DE ELEMENTOS DEBE SER DIVISIBLE POR EL Nº DE PROCESOS (p)! */
12 float x[ELEMENTOS], y[ELEMENTOS]; // VECTORES (VARIABLES GLOBALES)
13 /* FUNCION QUE CALCULA EL PRODUCTO ESCALAR LOCAL (DE UN TROZO DE LOS VECTORES) */
14 float prod_escalar_serie(
15 float a[], // ENTRADA
16 float b[], // ENTRADA
17 float n // ENTRADA: NUMERO DE ELEMENTOS
18 );
19 main(int argc, char** argv) {
20 int mi_rango; // RANGO DE MI PROCESO
21 int p; // NUMERO DE PROCESOS
22 int n = ELEMENTOS; // NUMERO DE ELEMENTOS DE LOS VECTORES
23 int n_local; // NUMERO DE ELEMENTOS DE CADA FRAGMENTO
24 int inicio_vector_local; // INDICE DE INICIO DE CADA FRAGMENTO
25 float suma_local; // PRODUCTO ESCALAR SOBRE MI INTERVALO
26 float suma_total; // PRODUCTO ESCALAR TOTAL
27 int fuente; // PROCESO QUE ENVIA RESULTADO DE SUMA
28 int dest = 0; // DESTINATARIO: TODOS LOS MENSAJES VAN A 0
29 int etiqueta = 0;
30 int i;
31 double ini, fin;
32 MPI_Status status;
33 MPI_Init(&argc, &argv); // INICIALIZA MPI
34 /* INICIALIZA LOS VECTORES */
35 ini = MPI_Wtime();
36 for (i = 0; i < n; i++)
37 x[i] = y[i] = i % 5;
38 MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango); // AVERIGUA EL RANGO DE MI PROCESO
39 MPI_Comm_size(MPI_COMM_WORLD, &p); // AVERIGUA CUANTOS PROCESOS HAY
40 n_local = n / p;
41 inicio_vector_local = mi_rango * n_local;
42 suma_local = prod_escalar_serie(&x[inicio_vector_local], &y[inicio_vector_local], n_local);
43 printf("MI RANGO = %d , SUMA LOCAL = %f\n", mi_rango, suma_local);
44 /* SUMA LAS CONTRIBUCIONES CALCULADAS POR CADA PROCESO */
45 if (mi_rango == 0) {
46 suma_total = suma_local;
47 for (fuente = 1; fuente < p; fuente++) {
48 MPI_Recv(&suma_local, 1, MPI_FLOAT, fuente, etiqueta, MPI_COMM_WORLD, &status);
49 suma_total = suma_total + suma_local;
50 }
51 fin = MPI_Wtime();
52 }
53 else {
54 MPI_Send(&suma_local, 1, MPI_FLOAT, dest, etiqueta, MPI_COMM_WORLD);
55 }
56 /* MUESTRA EL RESULTADO POR PANTALLA */
57 if (mi_rango == 0) {
58 printf("PRODUCTO ESCALAR USANDO p=%d TROZOS DE LOS VECTORES X E Y = %f\n", p, suma_total);
59 printf("Resultado tiempo %f\n", fin - ini);
60 }
61 MPI_Finalize(); // CIERRA EL UNIVERSO MPI */
62 } /* MAIN */
63 /* FUNCION QUE CALCULA EL PRODUCTO ESCALAR LOCAL (DE UN TROZO DE LOS VECTORES) */
64 float prod_escalar_serie(
65 float a[], // ENTRADA
66 float b[], // ENTRADA
67 float n /* ENTRADA: NUMERO DE ELEMENTOS */){
68 int i;
69 float suma = 0.0;
70 for (i = 0; i < n; i++)
71 suma = suma + a[i] * b[i];
72 return suma;
73 } /* PROD_ESCALAR_SERIE */

269
a) Funcionamiento del programa con número de elementos = 6 (Producto escalar = 30):

Nº de procesos: p =1 Rango:0 Suma local = 30 ] tiempo = 0' 000045 s

Rango:0 Suma local = 5


Nº de procesos: p =2 ] tiempo = 0' 000085 s
Rango:1 Suma local = 25
Rango:0 Suma local = 16
Nº de procesos: p =3 Rango:1 Suma local = 1 ] tiempo = 0' 000093 s
Rango:2 Suma local = 13

b) Líneas adicionales añadidas al programa o líneas que se han modificado:


MODIF 10 #define ELEMENTOS (2*3) // vale 6
NUEVA 31 double ini, fin;
NUEVA 35 ini = MPI_Wtime();
NUEVA 51 fin = MPI_Wtime();
NUEVA 59 printf("Resultado tiempo %f\n", fin - ini);

Tiempo de ejecución del programa (con nº elementos = 2*2*2*2*3*3*5*7*11*13 = 720720) al variar el número
de procesos (p) con el que es lanzado desde 1 hasta 6 procesos:

Nº procesos (p) Tiempo de ejecución Aceleración


1 0’004084 1’000000
2 0’005872 0’695504
3 0’004607 0’886477
4 0’006815 0’599266
5 0’009935 0’411072
6 0’010403 0’392579

¿Son lógicos los tiempos de ejecución que se observan?

Sí son lógicos, puesto que aunque la tarea de cómputo se reparte entre cada vez más
procesadores y por tanto el tiempo final para completarla disminuye, se producen tiempos de
penalización por fallos de acceso a caché que requiere acceso a memoria por tratarse de un
vector muy grande; de ahí el aumento del tiempo final.

Por otro lado, si aumentamos un número de procesadores y es mayor que del que disponemos
físicamente, se requiere tiempo de conmutación de procesos entre ellos; lo cual redunda
nuevamente en el aumento del tiempo requerido.

¿Cuáles son los tiempos que intervienen?

Entre los principales tiempos tenemos: tiempo de procesado, de acceso a memoria por fallo de
caché, de conmutación entre procesos y de mensajería entre procesos para su sincronización.

270
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 8 – Programación de computadores de memoria distribuida usando OpenMP.

Objetivos.

En esta práctica pretendemos ver cómo podemos usar OpenMP para paralelizar la ejecución de distintas
funciones, haciendo que cada hilo ejecute una función distinta.

Realización de la práctica.

Suponiendo que tenemos disponibles las siguientes herramientas:

 Visual Studio 2010, o cualquier otro compilador compatible con OpenMP C++
 Última versión de las librerías OpenCV: http://opencv.willowgarage.com/wiki/

Vamos a comparar los tiempos de ejecución necesarios para realizar las siguientes tareas. Partiendo de cuatro
imágenes en color y con resolución 640x480 (las imágenes pueden ser las mismas siempre que tengan nombres
distintos). Vamos a realizar el siguiente proceso:

1. Leemos las cuatro imágenes del disco (si es la misma podemos leerla una única vez, y obtener cuatro copias
de la imagen en cuatro variables tipo Mat distintas).

2. Empezamos la medida de los tiempos.

3. Sobre la primera imagen realizamos las siguientes operaciones en el siguiente orden:


a. Filtro de difuminado (Gaussian BLUR).
b. Conversión de color a escala de grises.
c. Cualquier función de Threshold.

4. Sobre la segunda imagen realizamos las siguientes operaciones en el siguiente orden:


a. Filtro de difuminado (Gaussian BLUR).
b. Conversión de color a escala de grises.
c. Ecualización del histograma.

5. Sobre la tercera imagen realizamos las siguientes operaciones en el siguiente orden:


a. Filtro de difuminado (Gaussian BLUR).
b. Conversión de color a escala de grises.
c. Le aplicamos el operador de Sobel.

6. Sobre la cuarta imagen realizamos las siguientes operaciones en el siguiente orden:


a. Filtro de difuminado (Gaussian BLUR).
b. Conversión de color a escala de grises.
c. Le aplicamos el operado de Laplace.

7. Terminamos la medida de tiempos.

8. Mostramos las imágenes resultado por pantalla.

271
Tened presente, que ni la lectura de datos, ni la muestra de resultados está incluida en la medida de los tiempos.
Si decidimos usar la misma imagen con cuatro copias, a cada copia hay que aplicarle los tres pasos
correspondientes.

Se pide:

1. Realizar la versión secuencial del algoritmo anterior midiendo los tiempos.

2. Realizar la versión paralela del algoritmo para 4 hilos midiendo los tiempos.

3. Comparar los tiempos obtenidos, explicando en detalle los resultados obtenidos y analizando los motivos de
las pérdidas en prestaciones de la versión paralela.

4. Suponiendo que tenemos una única imagen que procesar (y no cuatro como en el caso del apartado 2), y
que a dicha imagen le pretendemos aplicar los siguientes filtros en orden:
1. Conversión de color a escala de grises.
2. Cualquier función de Threshold.
3. Ecualización del histograma.
4. Aplicarle el operador de Sobel.

¿Podríamos usar la técnica anterior de paralelización? ¿Por qué?

5. Y si tuviéramos un flujo de imágenes (por ejemplo, un vídeo), y queremos aplicarle el proceso del apartado
4 a cada frame. ¿Cómo lo paralelizaríais? Dar una respuesta razonada desde un punto de vista teórico (no es
necesario que lo implementéis, pero podéis intentarlo).

Presentaciones de los resultados de la práctica.

Con todo lo anterior, trabajando en grupos de 2 alumnos, se deberá redactar una presentación (estilo Power
Point u OpenOffice Impress) de aproximadamente 8 minutos de duración, dividida en 2 partes aproximadamente
iguales.

La presentación debe incluir las respuestas a las preguntas del enunciado, indicando la manera en que se ha
escrito el código, los resultados de tiempo, su interpretación, así como cualquier descubrimiento interesante.

En la siguiente sesión, los alumnos realizarán la presentación de su trabajo (cada uno la mitad) y a continuación
ambos contestarán las preguntas del profesor y de los compañeros. Por tanto, todos los alumnos deben
prepararse la presentación entera

272
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 8 – Programación de computadores de memoria distribuida usando MPI.

Objetivos.

En esta práctica se pretende que el alumno conozca la metodología a emplear para desarrollar aplicaciones
paralelas para computadores paralelos de memoria distribuida y se familiarice con las particularidades que
comporta este tipo de plataforma distribuida para la ejecución de los programas, su depuración y su rendimiento.

Metodología.

Se estudiarán los fundamentos de la programación utilizando el paradigma más empleado hoy en día en este
tipo de plataforma: la biblioteca de funciones MPI (Message-Passing Interface), junto con el lenguaje de
programación C.

Se realizarán programas para un PC con sistema operativo Linux, usando el compilador gcc junto con la biblioteca
de funciones MPI. Primeramente el alumno empleará un único PC para aprender los mecanismos de desarrollo
y ejecución de programas paralelos que se emplean en plataformas distribuidas. En ese caso la ejecución en un
PC de la aplicación con distinto número de procesos podrá beneficiarse de los múltiples núcleos del procesador.
Como segundo paso, el alumno lanzará la ejecución de su aplicación paralela en varios PCs trabajando como si
fueran distintos nodos de un clúster y analizará su rendimiento.

Como documentación básica para trabajar usando el sistema operativo Linux, el alumno puede emplear el
documento “Comandos UNIX” escrito por el Centro de Cálculo de la E.T.S.I.I., que se encuentra en la URL:
http://www.informatica.us.es/index.php/en/servicios/centro-de-calculo/comandos-unix

Tras iniciar sesión en linux, abrir una consola de terminal y crear un directorio de trabajo para esta práctica,
llamado practica-mpi. Crear o editar los archivos de código fuente empleando cualquier editor de texto de Linux
(por ejemplo "vi", “nano” o “gedit”). Los comandos para compilar programas y ejecutar programas que usan MPI
son los siguientes:

Compilación: mpicc.openmpi [-lm] nombre_archivo.c -o nombre_archivo

El modificador opcional -lm hace que el compilador enlace el programa con las librerías matemáticas.

Ejecución: mpirun -np X nombre_archivo

Donde X es un número que indica el número de procesos que debe lanzar la aplicación.

273
Relalización de la práctica

Parte 1:

A continuación se presenta un programa en C que realiza una estimación


numérica de la integral o área encerrada entre la gráfica de una función no
negativa f(x), dos líneas verticales situadas en posiciones x=a y x=b y el eje x,
utilizando el “método de los trapecios”.

𝑆 = ∫ 𝑓(𝑥)𝑑𝑥
𝑎

El método de los trapecios consiste en subdividir el intervalo sobre el eje x en


n subintervalos idénticos y aproximar el área encerrada entre la curva y cada
subintervalo por un trapecio cuya base es el subintervalo, tal como se
muestra en la figura.

El área del trapecio i-ésimo se puede calcular fácilmente como la suma de las
áreas de un rectángulo y un triángulo:

1 1
𝑆(𝑡𝑟𝑎𝑝𝑒𝑐𝑖𝑜𝑖 ) = ℎ ∙ 𝑓(𝑥𝑖+1 ) + ℎ ∙ [𝑓(𝑥𝑖 ) − 𝑓(𝑥𝑖+1 )] = ℎ ∙ [𝑓(𝑥𝑖 ) + 𝑓(𝑥𝑖+1 )]
2 2

1 1 1
𝑆 ≈ ∑ 𝑆(𝑡𝑟𝑎𝑝𝑒𝑐𝑖𝑜𝑖 ) = ℎ ∙ [𝑓(𝑥0 ) + 𝑓(𝑥1 )] + ℎ ∙ [𝑓(𝑥1 ) + 𝑓(𝑥1 )] + ⋯ + ℎ ∙ [𝑓(𝑥𝑛−1 ) − 𝑓(𝑥𝑛 )]
2 2 2
𝑖
1
= ℎ ∙ [𝑓(𝑥0 ) + 2 ∙ 𝑓(𝑥1 ) + 2 ∙ 𝑓(𝑥2 ) + ⋯ + 𝑓(𝑥𝑛 )]
2
𝑓(𝑥0 ) 𝑓(𝑥𝑛 )
=[ + + 𝑓 (𝑥1 ) + 𝑓 (𝑥2 ) + ⋯ + +𝑓 (𝑥𝑛−1 )] ∙ ℎ
2 2

Esta es la fórmula de los trapecios empleada en el programa anterior, teniendo en cuenta que la base de todos
(𝑏−𝑎)
los trapecios tiene la misma longitud, que es: ℎ =
𝑛

274
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

/* integracion_trapecios_no_paralelo.c
* INTEGRACION NUMERICA POR EL METODO DE LOS TRAPECIOS
* ENTRADA: NINGUNA.
* SALIDA : ESTIMACION DE LA INTEGRAL DESDE a HASTA b DE f(x)
* USANDO EL METODO DE LOS TRAPECIOS CON n TRAPECIOS */
#include <stdio.h>
#include <math.h>
main(int argc, char** argv) {
float integral; // RESULTADO DE LA INTEGRAL
float a = -1.0; // EXTREMO IZQUIERDO
float b = 1.0; // EXTREMO DERECHO
int n = 1000; // NUMERO DE TRAPECIOS
float h; // LONGITUD DE LA BASE DEL TRAPECIO
float x;
float i;
float f(float x); /* FUNCION QUE ESTAMOS INTEGRANDO */
h = (b-a)/n;
integral = (f(a) + f(b))/2.0;
x = a;
for (i = 1; i <= n-1; i++) {
x = x + h;
integral = integral + f(x);
}
integral = integral*h;
printf("ESTIMACION USANDO n=%d TRAPECIOS,\n", n);
printf("DE LA INTEGRAL DESDE %f HASTA %f = %f\n\n", a, b, integral);
printf("ESTIMACION DE PI: %f\n", 2 * integral);
} /* main */
/* FUNCION QUE ESTAMOS INTEGRANDO */
/* CALCULA f(x) Y DEVUELVE SU VALOR */
float f(float x) {
float return_val;
return_val = sqrt(1 - x * x);
return return_val;
} /* f */

A partir de este programa, se pide que realice una versión paralela del mismo utilizando MPI. Para paralelizar el
programa, se deben distribuir los datos entre los diferentes procesadores. En este caso, los datos son los
trapecios dentro del intervalo [a, b]. La idea es que cada proceso (menos el de rango 0) estime el valor de la
integral sobre un subintervalo y envíe el resultado al proceso de rango 0, que se encargará de sumar todos los
resultados (y también computará el área del primer subintervalo).

Si tenemos en total n trapecios y p procesos (suponemos que n es mayor que p y además, por simplificar, que n
𝑛
es divisible por p), asignaremos 𝑝 trapecios a cada proceso. Así, cada proceso tendrá que aplicar la fórmula de
𝑛
los trapecios sobre un trozo del área total, de longitud ℎ ∙ 𝑝 . El proceso de rango 0 calculará la suma de los
𝑛
trapecios que van desde x = 𝑎 hasta x= 𝑎 + (ℎ ∙ 𝑝) . El proceso de rango 1, la suma de los trapecios que van
𝑛 𝑛
desde x = 𝑎 + (ℎ ∙ 𝑝) hasta x = 𝑎 + 2 ∙ (ℎ ∙ 𝑝) . Y así sucesivamente.

𝑛
Un proceso de rango genérico i calcularía la suma de los trapecios que van desde x = 𝑎 + 𝑖 ∙ (ℎ ∙ 𝑝) hasta
𝑛 𝑛
x = 𝑎 + (𝑖 + 1) ∙ (ℎ ∙ 𝑝) . Así pues, para este proceso su valor “a_local” sería 𝑎 + 𝑖 ∙ (ℎ ∙ 𝑝) y su valor “b_local”
𝑛 𝑛
sería 𝑎 + (𝑖 + 1) ∙ (ℎ ∙ ) . Asimismo, su número de trapecios a sumar, “n_local”, sería .
𝑝 𝑝

El algoritmo paralelo sería por tanto:

1. Cada proceso calcula cuál es su intervalo de integración, en función del valor de su rango i.
2. Cada proceso estima la integral de f(x) sobre su intervalo usando la regla de los trapecios.
3. Cada proceso distinto del 0 envía su integral al proceso 0.
4. El proceso 0 suma los valores recibidos de los procesos individuales y muestra el resultado final.

275
Probar el programa obteniendo una estimación del valor de π a partir del área de medio círculo. Es decir, calcular
el área de medio círculo de radio unidad. La función a integrar se obtiene teniendo en cuenta que la ecuación de
un círculo centrado en el origen es 𝑥 2 + 𝑦 2 = 𝑟 2 . Despejando y, resulta la función a integrar, que es:
2 𝜋
𝑦 = √𝑟 2 − 𝑥 2 . Sabemos que dicho área debe ser igual a 2 (ya que el área de un círculo de radio unidad es
𝜋 ∙ 𝑟 2 = 𝜋 . Por lo tanto, multiplicando por 2 el valor calculado por nuestro programa obtendremos una
estimación de π.

Solución 1: Utilizando Scatter y Gatter


#include <stdio.h>
#include <math.h>
#include <mpi.h>
#include <stdlib.h>

double f (double x);


void inicializa_trozos (double [], double, double, int);
double f2 (double *,int, double, int);

main (int argc, char** argv) {


double integral; // RESULTADO DE LA INTEGRAL
double a = -1.0; // EXTREMO IZQUIERDO
double b = 1.0; // EXTREMO DERECHO
int n = 10000; // NUMERO DE TRAPECIOS
double h; // LONGITUD DE LA BASE DEL TRAPECIO
double x;
double i;
double trozo[2];
int iterador = 0;
double sub_integral;
int mi_rango;
int p;
int tamanoTrozo;
double tiempo = 0.0;
double sub_integrales[p];

// Inicializa MPI
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango);
MPI_Comm_size(MPI_COMM_WORLD, &p);
tiempo = MPI_Wtime();
tamanoTrozo = 2;
double trozos [p*2];

/* FUNCION QUE ESTAMOS INTEGRANDO */


h = (b-a)/n;
inicializa_trozos(trozos,a, b, p);

MPI_Scatter(trozos, tamanoTrozo, MPI_DOUBLE, trozo, tamanoTrozo, MPI_DOUBLE, 0, MPI_COMM_WORLD);

sub_integral = f2(trozo, tamanoTrozo, h, n);

MPI_Gather(&sub_integral, 1, MPI_DOUBLE, sub_integrales, 1 , MPI_DOUBLE, 0, MPI_COMM_WORLD);

printf("ESTIMACION USANDO n=%d TRAPECIOS,\n", n);


printf("\nESTIMACION DE PI: %f\n", 2 * integral);

if(mi_rango == 0) {
sub_integral = 0.0;
for(iterador=0; iterador<p; iterador++) {
sub_integral += sub_integrales[iterador];
}
tiempo = MPI_Wtime() - tiempo;
printf("Algoritmo calculado en: %lf\n", tiempo);
}

MPI_Finalize();
}

276
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

/* FUNCION QUE ESTAMOS INTEGRANDO */


double f (double x) {
double return_val;
return_val = sqrt( 1 - x*x );
return return_val;
}

double f2 (double * trozo, int numElem, double h, int n) {


double return_val = 0.0;
int contador = 0;
int mi_rango;
int p;
int i = 0;
double a = trozo[0];
double b = trozo[1];

MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango);
return_val = (f(a) + f(b))/2.0;
MPI_Comm_size(MPI_COMM_WORLD, &p);
for(i=1;i<n/p;i++) {
return_val += f(a+(i*h));
contador++;
}
return_val = return_val*h;
return return_val;
}

void inicializa_trozos (double trozos [], double a, double b, int p) {


int i = 0;
double distancia = (b-a)/p;
double tmp = a;
int mi_rango;

for(i=0;i<p*2;i=i+2) {
if(i==0)
trozos[i] = tmp;
else
trozos[i] = trozos[i-1];
trozos[i+1] = tmp + distancia;
tmp = trozos[i+1];
}

PROCESOS TIEMPO
1 0,1752950
2 0,0913940
3 0,0641140
4 0,0501350
5 0,0443300
6 0,0368210
7 0,0323190

277
Solución 2: Utilizando Paso de mensajes
#include <stdio.h>
#include <math.h>
#include <mpi.h>

double f (double x);


double f2(double, double, double, int);

main (int argc, char** argv) {


double integral; // RESULTADO DE LA INTEGRAL
double a = -1.0; // EXTREMO IZQUIERDO
double b = 1.0; // EXTREMO DERECHO
int n = 10000; // NUMERO DE TRAPECIOS
double h; // LONGITUD DE LA BASE DEL TRAPECIO
double x;
double i;
int iterador = 0;
double sub_integral;
int mi_rango;
int p;
double inicio_vector_local;
double final_vector_local;
double distancia = 0.0;
double sub_total;
int fuente;
int etiqueta = 0;
double tiempo = 0.0;

MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango);
MPI_Comm_size(MPI_COMM_WORLD, &p);
tiempo = MPI_Wtime();

h = (b-a)/n;
distancia = (b-a)/p;
inicio_vector_local = a + mi_rango * distancia;
final_vector_local = a + (mi_rango+1) * distancia;
sub_integral = f2(inicio_vector_local, final_vector_local, h, n);
if (mi_rango == 0) {
sub_total = sub_integral;
for (fuente = 1; fuente < p; fuente++) {
MPI_Recv(&sub_integral, 1, MPI_DOUBLE, fuente, etiqueta, MPI_COMM_WORLD, &status);
sub_total += sub_integral;
}
printf("ESTIMACION USANDO n=%d TRAPECIOS,\n", n);
printf("\nESTIMACION DE PI: %lf\n", 2 * sub_total);
printf("%lf\n", (MPI_Wtime() - tiempo));
} else {
MPI_Send(&sub_integral, 1, MPI_DOUBLE, 0, etiqueta, MPI_COMM_WORLD);
}
MPI_Finalize();
}

double f (double x) { return sqrt( 1 - x*x ); }

double f2 (double inicio, double final, double h, int n) {


double return_val = 0.0;
int contador = 0;
int i = 0;
int mi_rango;
int p;
double a = inicio;
double b = final;
MPI_Comm_rank(MPI_COMM_WORLD, &mi_rango);
MPI_Comm_size(MPI_COMM_WORLD, &p);
return_val = f(a)/2.0 + f(b)/2.0;
for(i=1;i<n/p;i++) {
return_val += f(a+(i*h));
contador++;
}
return_val = return_val*h;
return return_val;
}

278
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Parte 2:

Modificar el programa realizado en el ejercicio anterior añadiéndole una medición del tiempo de ejecución,
usando la función MPI_Wtime.

Modificar también los datos de ejecución en el código fuente de modo que se evalúe:

 Extremo izquierdo: a = 1.0


 Extremo derecho: b = 100000.0
 Número de trapecios: n = 7927920. (Es el resultado de 2∙2∙2∙2∙3∙3∙5∙7∙11∙11∙13).
 Función que se integra: log(x)

El resultado del área debe ser aproximadamente 1.0 x 106 u2 , aunque los siguientes dígitos variarán debido a los
errores de truncamiento y a que se ha usado una precisión pequeña (variables de tipo float).

Con estos datos, ejecutar el programa y anotar el tiempo de ejecución obtenido para un número de procesos
desde 1 hasta 7. Representar una gráfica del tiempo de ejecución respecto al número de procesos.

La aceleración de un programa paralelo se define como el cociente entre el tiempo de ejecución del programa
para un sólo procesador y el tiempo de ejecución con n procesadores. Con los datos anteriores, calcular la
aceleración del programa para n desde 2 hasta 7 procesos. Representar una gráfica de la aceleración respecto al
número de procesos.

Interpretar ambas gráficas.

Solución:

Tabla de resultados de tiempos:

Tiempo de
ejecución
1 0,442766
Número de procesos

2 0,227557
3 0,226073
4 0,227226
5 0,212441
6 0,208168
7 0,198029

279
Solución: Utilizando Reducción.

#include <stdio.h>
#include <math.h>
#include <mpi.h>

main(int argc, char** argv) {


double integral, sub_integral = 0.0; // RESULTADOS DE LA INTEGRAL
float a_global = 1.0; // EXTREMO IZQUIERDO
float b_global = 100000.0; // EXTREMO DERECHO
int trapecios = 7927920; // NUMERO DE TRAPECIOS
float h; // LONGITUD DE LA BASE DEL TRAPECIO
double x;
float i;
double funcion(double x); // FUNCION QUE ESTAMOS INTEGRANDO

/* VARIABLES ADICIONALES */
double a_local, b_local; // EXTREMOS LOCALES DE CADA PROCESO
int num_proceso; // NUMERO DE PROCESO (0...p)
int procesos; // NÚMERO TOTAL DE PROCESOS
int num_trap_x_proc; // NUM. TRAPECIOS POR PROCESO
double Tini, Tfin; // MOMENTO DE INICIO Y FINAL DE EJECUCION

MPI_Init(&argc, &argv); // INICIO DE MPI


MPI_Comm_rank(MPI_COMM_WORLD, &num_proceso); // OBTENER NUMERO TOTAL DE PROCESOS
MPI_Comm_size(MPI_COMM_WORLD, &procesos); // OBTENER EL ID PROCESO ACTUAL

/* VARIABLE LOCALES */
h = (b_global - a_global) / trapecios;
num_trap_x_proc = trapecios / procesos;
a_local = a_global + h * num_proceso * num_trap_x_proc;

if (num_proceso == procesos - 1) {
b_local = b_global;
} else {
b_local = a_global + h * num_trap_x_proc * (num_proceso + 1);
}

Tini = MPI_Wtime();

/* CÁLCULO DE LA INTEGRAL */
sub_integral = (funcion(a_local) + funcion(b_local)) / 2.0;
x = a_local;
for (i = 0; i <= num_trap_x_proc - 2; i++) {
x = x + h;
sub_integral = sub_integral + funcion(x);
}
sub_integral = sub_integral * h;

MPI_Reduce(&sub_integral, &integral, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);

Tfin = MPI_Wtime();

if (num_proceso == 0) { // MASTER, OBTIENE LOS RESULTADOS DEL REDUCE Y LOS MUESTRA


printf("ESTIMACION USANDO n=%d TRAPECIOS,", trapecios);
printf("\nDE LA INTEGRAL DESDE %f HASTA %f = %f", a_global, b_global, integral);
printf("\nRESULTADO DE LOG: %f", integral);
printf("\NTIEMPO TOTAL REQUERIDO: %f\n", Tfin - Tini);
}

MPI_Finalize();
}

/* FUNCION QUE ESTAMOS INTEGRANDO */


double funcion(double x) {
return log(x);
}

280
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Parte 3:

Trabajando en grupos de 2 alumnos, lanzar la aplicación paralela desarrollada en la parte 2 sobre tres PCs del
aula. Los alumnos deberán configurar los ordenadores para ello, teniendo en cuenta las indicaciones que se
ofrecen a continuación e investigando en Internet para buscar información adicional.

Para que MPI sea capaz de lanzar procesos en otras máquinas, la cuenta debe estar configurada de modo que
MPI se pueda conectar remotamente en ellas, sin que se le pida contraseña, mediante una conexión segura
usando el protocolo de red criptográfico SSH (Secure Shell). Para ello hay que habilitar el acceso remoto sin
contraseña desde el nodo que lanza la aplicación paralela a todos los nodos adicionales sobre los que deba
ejecutarse.

Por ejemplo, si se quiere lanzar la aplicación paralela desde el nodo pc-14-145, corriendo además de en éste en
los nodos pc-14-146 y pc-14-147, el procedimiento es:

 Activar el demonio SSH en todos los nodos adicionales donde se vaya a correr el programa. Ejecutar para
ello el siguiente comando en cada uno de ellos:
sudo service ssh start

 Generar la pareja de claves SSH pública y privada del equipo que lanza el programa (tipo DSA). Para ello,
desde el nodo pc-14-145, ejecutar:
ssh-keygen -t dsa

 Copiar e instalar la clave pública en todos los ordenadores adicionales donde se vaya a correr el
programa. Desde el nodo pc-14-145, hacer:
ssh-copy-id practica@pc-14-146
ssh-copy-id practica@pc-14-147

 Desde el equipo que lanzará el programa, copiar el directorio completo de trabajo donde esté incluido
el ejecutable a todos los ordenadores adicionales donde se vaya a correr el programa. Por ejemplo, si
dicho directorio es /home/practica/mpi-001 habrá que situarse en /home/practica y ejecutar:
scp -r ./mpi-001 practica@pc-14-146:/home/practica
scp -r ./mpi-001 practica@pc-14-147:/home/practica

 Ejecutar la aplicación paralela indicando explícitamente el nombre (o dirección IP) de todos los nodos
sobre los que se vaya a ejecutar en paralelo. Desde el nodo pc-14-145:
mpirun -np 7 -host pc-14-145, pc-14-146, pc-14-147 /home/practica/mpi001/ejercicio2

Con los mismos datos de ejecución indicados para el apartado 2, ejecutar el programa y anotar el tiempo de
ejecución obtenido para un número de procesos desde 1 hasta 7. Representar una gráfica del tiempo de
ejecución y otra de la aceleración respecto al número de procesos.

Interpretar ambas gráficas, comparándolas con las obtenidas en la parte 2. Razonar a qué se deben las diferencias
encontradas. ¿Se ha conseguido sacar rendimiento a la posibilidad de conectar entre sí varios PCs, tal como en
un clúster, para ejecutar aplicaciones paralelas?

281
Solución:

Tiempo de ejecución
Aceleración
Sin distribuir Con distribución
1 0,442766 0,1453270912 1,000000000
2 0,227557 0,0813248158 1,786995639

Número de
procesos
3 0,226073 0,0580251217 2,504554699
4 0,227226 0,0472111702 3,078235311
5 0,212441 0,0380239487 3,821988409
6 0,208168 0,0332419872 4,371793128
7 0,198029 0,0286531448 5,071942093

282
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Práctica 9 – Configuración de clúster FTP con balanceo de carga.

Objetivos.

El objetivo de la presente práctica es la configuración de un servidor FTP en alta disponibilidad y balanceo de


carga desplegado sobre un clúster basado en máquinas virtuales.

 Configuración de las máquinas virtuales del clúster sobre el que se desarrollará la práctica.
 Configuración de servicio FTP.
 Configuración de un balanceador de carga basado en Linux Virtual Server (LVS).
 Realización de pruebas básicas para comprobar el correcto funcionamiento del servicio.

Introducción.

El objetivo de la práctica es trabajar sobre un clúster de máquinas GNU/Linux (en este caso máquinas virtuales),
que en su conjunto prestan un servicio de transferencia de ficheros FTP. Al disponerse de varias máquinas, se
dotará al servicio FTP de redundancia, la cual constituye una de las formas que tenemos a nuestro alcance para
dotar de continuidad y disponibilidad a este servicio.

La configuración propuesta en esta práctica está basada en el uso de Linux Virtual Server (LVS), un proyecto open
source cuyo objetivo es facilitar la prestación de servicios en alto rendimiento y disponibilidad mediante el uso
de clústeres. Los componentes de LVS dependen del framework Linux Netfilter, especializada en el manejo de
paquetes de red.

El clúster propuesto está compuesto por una serie de máquinas que proporcionan servicio de FTP y una máquina
que hace las labores de balanceador de carga (Load Balancer, LB), actuando como receptor de las peticiones FTP
y siguiendo un algoritmo de balanceo, las redirigirá hacia los distintos servidores FTP del clúster. De este modo
se reparte la carga entre os servidores y en caso de que uno de ellos falle, el balanceador puede detectarlo y
enviar las peticiones a cualquiera de las otras máquinas que aún se mantienen prestando servicio.

Estructura e información sobre el clúster.

Máquinas.

Las máquinas del clúster (servidores FTP y balanceador) serán máquinas virtuales ejecutándose sobre VMware
Player, el cuál proporciona tecnología de virtualización de hardware mediante la cual una máquina física
(máquina Host) puede albergar varias máquinas virtuales (máquinas Guest). Estas máquinas virtuales son
elementos software que simulan el comportamiento de un ordenador físico y pueden ejecutar programas como
si de una máquina real se tratase. El clúster propuesto consta de 3 máquinas virtuales inicialmente idénticas
sobre las que se realizará una serie de modificaciones de tal forma que una de ellas actuará como balanceador
de carga basado en LVS y las otras dos ejecutarán sendos servidores de FTP.

Las especificaciones de las máquinas virtuales son las siguientes:

- 1 procesador mononúcleo.
- 256 MB memoria RAM.
- 10 GB disco duro virtual.
- Adaptador de red en modo NAT.
- Sistema operativo Debian 7 Wheezy 32 bits.

283
Además del sistema base Debian, se encuentra preinstalado el servidor de FTP vsftpd con la configuración por
defecto proporcionada por Debian. Los usuarios necesarios para manejar las máquinas son:

Usuario: root
Clave: clave.root
Privilegios: Usuario administrador del sistema

Usuario: usuario
Clave: clave.usuario
Privilegio: Usuario del sistema sin privilegios especiales

Por motivos de tamaño, las máquinas virtuales no cuentan con entorno gráfico, por lo que la administración y
configuración de las mismas se realizará mediante terminal de consola. En la siguiente URL se encuentra una
breve recopilación de los comandos más comunes para el manejo de la terminal:

http://www.informatica.us.es/index.php/en/servicios/centro-de-calculo/comandos-unix

En caso de usar una máquina anfitriona ejecutando Windows, la conexión a las máquinas virtuales mediante
protocolo SSH se puede realizar mediante la aplicación PuTTY, de código abierto y que no precisa instalación:

http://the.earth.li/~sgtatham/putty/latest/x86/putty.exe

Desde entornos GNU/Linux, cualquier emulador de consola nos permitirá conectar vía SSH a las máquinas.

Red

VMware permite la conexión en modo NAT de las máquinas virtuales mediante la cual todas ellas se encuentran
en una red privada virtual donde tienen visibilidad unas sobre otras y todas son accesibles a su vez desde la
máquina anfitriona donde se están ejecutando.

Las máquinas del clúster están configuradas en modo NAT, por lo que tanto balanceador como servidores FTP se
encuentran conectados a la misma red local virtual. El clúster será identificado por una dirección IP virtual (VIP)
que no se corresponderá con ninguna de las máquinas virtuales, pero será el balanceador el encargado de
capturar las peticiones dirigidas a la VIP y redireccionarlas convenientemente.

Por simplicidad, las máquinas no contendrán ninguna regla de cortafuegos. Si bien esto supone un detrimento
de la seguridad, por tratarse de una prueba de concepto a desplegar en un entorno controlado, no supondrá
mayores problemas mientras que simplificará el despliegue de los servicios.

284
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Realización de la práctica.

Sistema operativo y entorno de trabajo.

El sistema operativo de la máquina anfitriona del clúster no es relevante para el desarrollo de esta práctica. Las
herramientas de virtualización de VMware están disponibles tanto para sistemas Windows como GNU/Linux, por
lo que el alumno puede decantarse por cualquiera de los dos sistemas a la hora de trabajar en casa. En el
laboratorio VMware Player únicamente se encuentra instalado en las imágenes Windows, por lo que esta será la
opción elegida para el desarrollo de la práctica y hacia la que irán orientados los procedimientos descritos, si
bien se acompañarán de las instrucciones alternativas para su realización en GNU/Linux.

En los apéndices al final de la práctica el alumno encontrará información relevante a las distintas herramientas
de administración necesarias para realizar la práctica: instrucciones para la edición de fichero, comandos de
terminal, copiado de máquinas virtuales y cualquier otra información considerada relevante.

NOTA: en caso de trabajar directamente con las ventanas de las máquinas en VMware, el cursor del ratón y
teclado queda capturad dentro de dichas ventanas. Para liberarlos y poder seguir trabajando con la máquina
anfitriona es necesario pulsar la combinación de teclas Ctrl Izquierdo + Alt Izquierda.

Ejercicio 1: Creación de las máquinas virtuales.

La primera tarea consiste en crear tres nuevas máquinas virtuales VMware a partir de la plantilla
debian_template, proporcionada para la realización de esta práctica. Los nombres recomendados para estas
nuevas máquinas virtuales son: debian_00, debian_01, debian_02. Una vez creadas, deberán abrirse a través de
la aplicación VMware Player para poder ser usadas en los posteriores apartados.

Los ordenadores del laboratorio de prácticas están equipados con el software VMware Player, una herramienta
gratuita distribuida por VMware que proporciona las funcionalidades básicas para la creación y manejo de
máquinas virtuales.

Para la presente práctica se han creado tres máquinas virtuales idénticas pero con distinto nombre para crear el
clúster virtualizado. Estas máquinas se denominan debian_00, debian_01 y debian_02 y están disponibles en las
siguientes URL de descarga, comprimidas en formato zip:

http://www.atc.us.es/descargas/debian_00_ok.zip
http://www.atc.us.es/descargas/debian_01_ok.zip
http://www.atc.us.es/descargas/debian_02_ok.zip

NOTA: Para descomprimir desde línea de comandos puede usarse la orden unzip archivo.zip, la cual
descomprimirá en el directorio de trabajo el contenido del archivo comprimido.

Las nuevas máquinas virtuales deben abrirse usando la aplicación VMware Player, a través del menú File, opción
Open Virtual Machine. La primera vez que arranquen estas nuevas máquinas virtuales, VMware detectará que
se tratan de máquinas duplicadas, preguntando al usuario si se tratan de máquinas que han cambiado de
ubicación o bien de copias independientes. En nuestro caso indicaremos es necesario indicar que se tratan de
copias a fin de que VMware pueda cambiar ciertos atributos únicos de la máquina como la dirección MAC de las
tarjetas de red virtuales.

NOTA: VMware Player no cuenta con la funcionalidad que nos permite copiar/clonar máquinas virtuales, por lo
que el proceso debe hacerse a mano. Para el copiado de máquinas virtuales VMware en sistemas Windows y
GNU/Linux, consultar el Apéndice B de la presente práctica.

285
Ejercicio 2: Configuración de red de las máquinas virtuales.

Editar la configuración de red de las máquinas virtuales para que cada máquina tenga asignada una IP estática
dentro de la red privada virtual a la que pertenecen las máquinas. Seleccionar también una IP para ser usada
como IP virtual (VIP). Esta IP no identificará a ninguna máquina concreta, sino al clúster en su conjunto como
entidad proporcionando un servido de FTP.

Las máquinas virtuales del clúster se encuentran conectadas a una red local virtual donde obtienen
dinámicamente mediante DHCP una IP correspondiente a una IP de red privada. Esta red se conecta al exterior
gracias a una configuración en NAT. Dependiendo de varios factores (configuración VMware player, versión, etc.),
el rango de IP privadas puede variar, pudiendo ser del tipo 192.168.X.X, 172.16.X.X, etc.

Debido a que las direcciones IP se asignan de forma dinámica, no tenemos asegurado que los servidores reciban
siempre la misma IP, por lo que el primer paso en nuestra configuración es cambiar la asignación dinámica de IP
por una IP estática dentro del mismo rango al que pertenece la red privada virtual a la que se conectan las
máquinas.

NOTA: se recomienda usar una consola independiente (en la máquina anfitriona), en lugar de usar la ventana
VMware donde se ejecuta la máquina virtual. Para entornos Windows, recurrir a la herramienta PuTTY.

Antes de modificar la configuración de red, es necesario tomar nota de la configuración actual de alguna de las
máquinas. Para ello, una vez conectados a cualquiera de las máquinas virtuales es necesario ejecutar los
comandos:
> ifconfig eth0
> route -n

De los datos que aparece, es necesario recordar los relativos a

• net addr Dirección IP de la tarjeta de red, necesaria para conocer el rango de la red privada.
• Mask Máscara de red.
• Gateway Puerta de enlace, sólo anotar datos de Gateway de la entrada con Destination 0.0.0.0

Con la información anterior, es necesario elegir 4 direcciones IP, una para cada una de las máquinas virtuales y
una cuarta para identificar a todo el clúster. Si la IP devuelta por ifconfig fue del tipo 192.168.2.X, la elección de
direcciones podría ser del tipo:
192.168.2.10 Dirección IP para identificar al clúster.
192.168.2.20 Dirección IP para el balanceador.
192.168.2.21 Dirección IP para el servidor 1 de FTP.
192.168.2.22 Dirección IP para el servidor 2 de FTP.

El resto de parámetros de red (dns, gateway, netmask) son idénticos para las 3 máquinas. Una vez obtenidos los
anteriores datos, es necesario editar el fichero /etc/network/interfaces de las tres máquinas virtuales. Se
recomienda el uso del editor de texto en consola nano por su simplicidad (Apéndice A). La configuración de la
interfaz de red eth0 debe quedar de la siguiente forma una vez editada:
# The primary network interface
allow-hotplug eth0
iface eth0 inet static
address ip.estatica.red.máquina
netmask mascara.red
gateway puerta.enlace
dns-nameservers 8.8.8.8

286
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Para aplicar los cambios es necesario reiniciar el servicio de networking. Para ello tenemos dos opciones:

 Reiniciar la máquina virtual mediante el comando reboot.

 Reiniciar el servicio networking desde la consola de la máquina en la ventana de VMware Player (si se
intenta desde una terminal de la máquina anfitriona, se perderá la conexión con la máquina virtual). Los
comandos necesarios para reiniciar la actividad de red son:
> service networking stop
> service networking start

Ejercicio 3: Configuración del servidor FTP.

Configurar adecuadamente los dos servidores FTP del clúster y ponerlos a funcionar de forma independiente. El
servidor FTP deberá ser configurado para cumplir con los siguientes requisitos:

 No se permitirán conexiones anónimas al FTP.


 Los usuarios locales de la máquina, como por ejemplo “usuario”, tendrán acceso al servidor FTP.
 Los usuarios podrán descargar y subir ficheros al servidor FTP.

Además de la configuración, deberá realizarse la subida y descarga de varios ficheros de prueba accediendo con
la cuenta “usuario”.

El File Transfer Protocol (FTP) es un protocolo de red estándar usado para la transmisión de archivos desde una
máquina hasta otra a través de redes basadas en TCP, como por ejemplo Internet. En esta práctica ha sido elegido
como el servicio al que se le dotará de redundancia. Éste servicio es proporcionado por un software específico
que se ejecutará en el servidor, el servidor de FTP. Si bien existen diversas implementaciones independientes del
protocolo FTP, hemos elegido una de las más extendidas, vsftpd, que se encuentra instalado por defecto en las
máquinas virtuales. Sólo 2 de las tres máquinas virtuales ejecutarán el servicio de FTP, la máquina elegida como
balanceadora no necesita que sea configurado.

Para permitir el correcto funcionamiento del protocolo FTP, es necesario cargar dos módulos del kernel Linux.
Para ello se ejecutarán las instrucciones:
> modprobe ip_conntrack
> modprobe ip_conntrack_ftp

Para asegurarnos que estos módulos se carguen automáticamente cuando arranque la máquina, se añadirán los
nombres de los módulos (ip_conntrack, ip_conntrack_ftp) en líneas independientes dentro del fichero
/etc/modules.

La configuración de cada uno de los servidores FTP se realizará editando los respectivos ficheros de configuración
de vsftpd, concretamente /etc/vsftpd.conf. Como en anteriores ocasiones, se recomienda la edición usando el
editor nano.

Para que los cambios en el archivo de edición surtan efecto, es necesario reiniciar el servicio de FTP, para ello se
puede hacer uso de la instrucción:
> service vsftpd {start|stop|restart}

indicando en cada caso la acción que se desea realizar con el servicio especificado.

287
Para comprobar el correcto funcionamiento de cada uno de los dos servidores FTP, se pide subir un fichero a
cada uno de los servidores FTP. El nombre de cada fichero será distinto. Para realizar esta acción, puede usarse
desde la máquina anfitriona el software cliente de FTP Filezilla, disponible tanto para sistemas Windows como
GNU/Linux. Los datos de conexión a usar son:

 Servidor: IP del servidor FTP


 Usuario: usuario
 Clave: clave.usuario

NOTA: las máquinas del laboratorio no permiten la instalación del software Filezilla, por lo que se recomienda
usar la versión portable de este software, la cual no requiere privilegios de administrador para su instalación. El
instalador puede descargarse de la siguiente URL: https://consigna.us.es/226208 clave: clave.asd

Ejercicio 4: Configuración de Linux Virtual Server (LVS)

Configurar la máquina balanceadora de carga para que distribuya las conexiones FTP entrantes entre las
máquinas disponibles en el clúster. La máquina balanceadora se encargará únicamente de captar las peticiones
al clúster y redirigirlas a las máquinas correspondientes siguiendo un algoritmo de planificación especificado.

Se pide crear un script que configure el balanceador y se ejecute automáticamente durante el arranque de la
máquina, así como dar persistencia a las reglas de cortafuegos necesarias en las máquinas servidoras.

Linux Virtual Server (LVS) es una herramienta open source que permite la creación de clúster Linux de alto
rendimiento y disponibilidad. Una de las líneas de trabajo del proyecto es IPVS (IP Virtual Server) que implementa
balanceo de carga en el kernel Linux a nivel de capa de transporte (Layer-4 LAN switching).

IPVS forma parte del kernel de Linux, por lo que sus funcionalidades están disponibles junto con los kernels
empaquetados por la mayoría de las distribuciones actuales. IPVS hace que el clúster sea visto desde el exterior
como una única entidad proveyendo un servicio concreto a través de una IP virtual (VIP), el balanceador se
encarga de redirigir a las máquinas reales del clúster las peticiones de servicios basados en TCP/UDP que se
envían a la VIP.

En primer lugar la máquina balanceadora debe ser capaz de redirigir paquetes hacia otras máquinas, por lo que
es necesario activar la siguiente opción usando el comando:
> echo “1” > /proc/sys/net/ipv4/ip_forward

El siguiente paso es hacer que la máquina balanceadora escuche todas las peticiones dirigidas hacia la Virtual IP
(VIP) que identifica a todo el clúster. Para ello, hay que añadir una segunda dirección IP a la interfaz de red eth0:
> ifconfig eth0:1 VIP netmask X.X.X.X

donde VIP es la IP virtual elegida para el clúster y X.X.X.X la máscara de red correspondiente a la red privada en
la que se encuentran las máquinas virtuales.

Llegados a este punto, necesitamos instalar la herramienta de administración ipvsadm, la cual está disponible en
los repositorios de Debian. Para ello es necesario ejecutar:
> apt-get install ipvsadm

Una vez instalada, es conveniente purgar todas las reglas de balanceo que pudiesen existir antes de introducir
las nuevas. Esto se logra mediante la instrucción:
> ipvsadm -C

288
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

A continuación se definirá un servicio al que posteriormente se agregarán las reglas de balanceo. Para ello es
necesario ejecutar:
> ipvsadm -A -t VIP:puerto -s alg.scheduling

el manual de la herramienta contiene una descripción más detallada de los parámetros que acepta (man
ipvsadm). Los parámetros correspondientes a la anterior instrucción son:

 -A Añadir servicio.
 -t Indica que el servicio está basado en TCP.
 VIP Virtual IP, dirección IP que identifica a todo el clúster.
 Puerto Puerto usado por el servicio, puede ser valor numérico o identificador de servicio.
 -s alg.scheduling Especifica el algoritmo de planificación usado para el balanceo de carga.

Una vez creado el servicio, hay que asociar las máquinas servidoras con el servicio que hemos definido en el
balanceador (en este caso, añadir 2 servidores FTP). El esqueleto de la instrucción es el siguiente:
> ipvsadm -a -t VIP:puerto -r IP.servidor:puerto -g

los parámetros de la anterior instrucción se corresponden con:

 -a Añade un servidor al clúster controlado por LVS.


 -t Servicio basado en TCP.
 IP.servidor IP del servidor que se desea añadir al clúster.
 Puerto Puerto del servicio, en este caso, es el mismo que el usado para definir el servicio.
 -g Indica la configuración de red del clúster. En nuestro caso, es el denominado Direct
Connection o Gateway.

Complementariamente, hay que desactivar el servicio FTP en el balanceador, pues sólo redireccionará peticiones
FTP, no las atenderá. Para ello hay que recurrir al comando service como se ha visto anteriormente. Para
asegurarse de que el servidor de FTP no arranca automáticamente cuando se inicie la máquina, se puede recurrir
al comando rcconf (específico para Debian y derivados).

Finalmente, es necesario configurar el cortafuegos de las máquinas servidoras de FTP para que sepan
redireccionar correctamente las respuestas a las peticiones FTP. Para ellos es necesario añadir la siguiente regla
de cortafuegos:
>iptables -t nat -A PREROUTING -p tcp -d VIP --dport ftp -j REDIRECT

donde VIP debe ser sustituido por la IP que identifica al clúster.

289
Ejercicio 5: Pruebas sobre el clúster FTP con balanceo de carga.

Comprobar el correcto funcionamiento del clúster y contestando a las preguntas formuladas en las plantillas
entregables que acompañan al boletín de prácticas.

Realización:

A fin de realizar una monitorización básica del tráfico que recibe cada balanceador, usaremos el analizador de
paquetes de red tcpdump. Para instalarlo, ejecutar la siguiente instrucción:
> apt-get install tcpdump

Iniciar sesión en cada una de las máquinas servidoras de FTP (usar dos terminales de consola). En cada sesión,
ejecutar como administrador la instrucción
> tcpdump -n -i any port ftp

la cual monitoriza todos los paquetes FTP que pasen a través de las interfaces de red de la máquina. Cada vez
que se detecte uno de los paquetes especificados, se mostrará una nueva línea por pantalla con su información.
Una vez esté funcionando esta monitorización básica, contestar las preguntas formuladas en las plantillas.

Apéndice A: Edición de ficheros.

A lo largo de la práctica será necesario editar archivos de configuración contenidos en las distintas máquinas
virtuales, que como se mencionó anteriormente, carecen de entorno gráfico. Existen diversos editores en modo
texto que pueden usarse sin necesidad de entorno gráfico. Por motivos de espacio, las máquinas virtuales sólo
incluye los editores vim y nano. En caso de no contar con experiencia previa con el primero de ellos, nano es la
opción más amigable para el usuario, contando a pie de pantalla con un listado de los distintas combinaciones
de teclas necesarias para llevar las distintas acciones de apertura, cierre, guardado de datos, etc.

Las opciones más relevantes del editor nano son las siguientes:

^O: Ctrl + O Guarda cambios en el fichero.

^K: Ctrl + K Recorta la línea donde esté situado el cursor.

^U: Ctrl + U Pega el contenido del portapapeles en la línea actual.

^W: Ctrl + W Busca la cadena de texto que se le indique a continuación.

^X: Ctrl + X Sale de la aplicación, preguntando si se desean guardar los cambios.

290
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Apéndice B: Copiado manual de máquinas virtuales VMware.

En VMware, una máquina virtual consta de un directorio cuyo nombre se corresponde con la máquina virtual y
que contiene los archivos de configuración que la definen (.vmx, .vmxf), archivos de datos que contiene la
información del disco duro virtual de la máquina (.vmdk) así como otros ficheros auxiliares (logs, volcados de
memoria, etc.).

Debido a que VMware Player sólo proporciona funcionalidades básicas para el manejo de máquinas virtuales, la
clonación o copiado de máquinas no está incluida entre ellas. Las soluciones pasan por usar alguna de las
herramientas de pago de su suite de virtualización (VMware Workstation) o bien realizar manualmente el
proceso.

A continuación se describe el proceso para realizar el copiado/clonado manual de máquinas virtuales. Se


adjuntan propuestas de comandos para terminal GNU/Linux para realizar cada uno de los pasos indicados. En
caso de que el alumno decida realizar esta operación en entorno Windows, sólo tendrá que adaptar el proceso
a las herramientas proporcionadas por su sistema operativo. Los pasos a seguir son los siguientes:

1. Realizar una copia completa del directorio que contiene la máquina virtual. El nuevo directorio llevará el
nombre de la nueva máquina que estamos creando. En Windows se puede realizar esta acción desde el
explorador de ficheros. El comando para terminal de GNU/Linux es el siguiente:
> cp -r maquina_original maquina_nueva

2. Acceder al directorio de la nueva máquina virtual y renombrar todos los archivos de tal forma que el nombre
de la máquina original que aparece en los nombres de fichero sea sustituido por el nombre de la nueva
máquina. En Windows se puede realizar esta acción desde el explorador de ficheros, renombrando los
elementos uno a uno. En GNU/Linux se puede usar el comando rename de la siguiente forma:
> cd ./maquina_nueva
> rename 's/maquina_original/maquina_nueva/' *

3. Editar el contenido de los siguientes ficheros: <nueva_maquina>.vmx; <nueva_maquina>.vmxf;


<nueva_maquina>.vmdk; de tal forma que cualquier referencia al nombre de la antigua máquina virtual se
sustituya por el de la nueva que estamos creando. En Windows se puede realizar esta acción usando un
editor de texto básico como Notepad, en GNU/Linux se puede usar el comando sed de la siguiente forma:
> sed -i 's/maquina_original/maquina_nueva/g' maquina_nueva.vmx
> sed -i 's/maquina_original/maquina_nueva/g' maquina_nueva.vmxf
> sed -i 's/maquina_original/maquina_nueva/g' maquina_nueva.vmdk

Para entornos GNU/Linux se ha definido el script para consola de comandos, clone.sh, que automatiza el proceso
en entornos GNU/Linux partiendo de los comandos descritos con anterioridad. La invocación del script debe
realizarse de la siguiente manera:
./clone.sh [ruta_maquina_virtual] maquina_original maquina_copia

Los parámetros que toma el script son los siguientes:

[ruta_maquina_virtual] Indica el directorio que contiene la máquina virtual que queremos clonar. Si el
script se encuentra en el mismo directorio, no es necesario especificar este parámetro.

maquina_original Nombre de la máquina virtual de la que se quiere obtener una copia.

maquina_copia Nombre de la nueva máquina virtual que se creará a través del proceso de copiado.

Ejemplo: Dada la máquina virtual debian_template, el comando para crear una nueva máquina denominada
debian_01 sería el siguiente (suponemos que la máquina virtual se encuentra en la ruta $HOME/p9):
> ./clone.sh $HOME/p9 debian_template debian_01

291
RESULTADOS

ASD: ARQUITECTURA DE SISTEMAS DISTRIBUIDOS. 3º GTI.

PRÁCTICA 9. CONFIGURACIÓN DE CLUSTER FTP CON BALANCEO DE CARGA

Nombre:

Ejercicio 1: Realice un esquema del clúster donde se aprecie la estructura de red, rangos de IP e interconexión
entre las distintas máquinas (servidores, balanceador y máquina anfitriona.

HOST-0
BALANCEADOR
VIP: 192.168.205.10
IP: 192.168.205.20

HOST-1 HOST-2
FTP 1 FTP 2
IP: 192.168.205.21 IP: 192.168.205.22

ANFITRIÓN
CLUSTER VIP: 192.168.205.10
IP: 10.1.12.95

Nota: Todas las máquinas se encuentran interconectados en la misma red. Las flechas indican el balanceo FTP.

Ejercicio 2: Indicar la configuración de red de cada una de las máquinas (IP, netmask, gateway, …)

NOMBRE FUNCIÓN VIP IP Máscara Puerta de enlace


ANFITRIÓN Soporte 010.001.012.095 255.255.252.000 010.001.012.001
------ Clúster 192.168.205.010 255.255.255.000
HOST-0 Balanceador 192.168.205.010 192.168.205.020 255.255.255.000 192.168.205.002
HOST-1 Serv. FTP #1 192.168.205.021 255.255.255.000 192.168.205.002
HOST-2 Serv. FTP #2 192.168.205.022 255.255.255.000 192.168.205.002

292
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Ejercicio 3: Las máquinas que dan servicio FTP cuentan con usuarios y contraseñas independientes (“root” y
“usuario”), son estos usuarios los únicos que pueden conectarse a la dirección de FTP.

¿Qué ocurre si añadimos un nuevo usuario únicamente en una de las máquinas y queremos acceder con dicho
usuario a través del balanceador?

Al acceder al servicio balanceado de FTP con un usuario que sólo está en algunas máquinas del
clúster podría causar error de inicio de sesión si el equipo balanceador le asignase una máquina
de servidor FTP donde no esté creado ese usuario.

¿Qué requisitos han de cumplirse para que un usuario pueda acceder a su cuenta FTP dentro del clúster sin
tener problemas de autentificación, independientemente de a qué maquina se redirija su petición?

Las máquinas de componen el clúster de servicio de FTP deberían de tener los mismos usuarios,
esto es, alguna forma de sincronizar los usuarios entre dichas máquinas.

Ejercicio 4a: Escriba el script necesario para configurar el LVS e indique la configuración necesaria en el sistema
para que arranque automáticamente al iniciar la máquina.
#!/bin/sh
echo "1" > /proc/sys/net/ipv4/ip_forward
ifconfig eth0:1 192.168.205.10 netmask 255.255.255.0
ipvsadm –C
ipvsadm -A -t 192.168.205.10:21 -s rr
ipvsadm -a -t 192.168.205.10:21 -r 192.168.205.21:21 –g
ipvsadm -a -t 192.168.205.10:21 -r 192.168.205.22:21 -g

Nota: Para que se ejecutara en el arranque podemos incluir la siguiente línea en el archivo /etc/rc.local:
/bin/sh ~/ejercicio4a.sh

Ejercicio 4b: Realice un script para activar la que debe añadirse a los cortafuegos de los servidores FTP e
indique la configuración necesaria para que se ejecute el script automáticamente al iniciar la máquina.
#!/bin/sh
iptables -t nat –F
iptables -t nat -a PREROUTING -p tcp -d 192.168.205.10 --dport ftp –j REDIRECT

Nota: Para que se ejecutara en el arranque podemos incluir la siguiente línea en el archivo /etc/rc.local:
/bin/sh ~/ejercicio4b.sh

293
Ejercicio 5a: Una vez tcpdump está monitorizando las conexiones FTP en cada uno de los servidores FTP,
realizar desde diversas conexiones FTP contra la IP virtual del clúster. ¿Qué algoritmo de balanceo se eligió en
la configuración de LVS? ¿Se corresponde el balanceo observado con el esperado?

El algoritmo de balanceo elegido es Round-Robin.

Si, balanceo observado se corresponde con el esperado.

Ejercicio 5b: Subir un archivo mediante Filezilla usando la dirección VIP del clúster. Finalizar la conexión e
iniciar una nueva, comprobando si el archivo se sigue encontrando en el servidor.

NOTA: para visualizar correctamente el contenido del repositorio FTP, será necesario pulsar F5 entre conexión
y conexión para refrescar el contenido mostrado.

¿Qué ocurre si se repite la desconexión/conexión/comprobación varias veces?

Al tener establecido el algoritmo Round-Robin, el archivo subido aparece de forma alternativa


según la conexión. Esto es, el archivo está subido a una única máquina servidora pero nuestra
conexión permuta entre las dos máquinas existentes, encontrando el archivo sólo cuando nos
conectamos a la que realmente lo contiene, es decir, de forma altertante.

¿A qué se debe este comportamiento observado y cómo se podría solucionar?

Para solucionar este problema debería de replicarse todas las acciones en el resto de máquinas.
Por ejemplo se podrían sincronizar los archivos entre todas las máquinas con un programa
externo.

294
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Apéndice: Configuración de Visual Studio 2010 para OpenCV.

Instalación básica de OpenCV para Windows 7.

1. Obtenga la última versión estable de OpenCV (actualizable cada año) desde:


http://sourceforge.net/projects/opencvlibrary

Nota: Obtenga una versión posterior a la 2.3, ya que versiones anteriores sólo soportan 32-bits y no
incluyen soporte TBB (por lo no podrá utilizar múltiples cores a menos que multiprocese su propia
aplicación). Por lo que se recomienda reconstruir las versiones anteriores de código fuente usando
CMake con el mismo compilador que haya utilizado en sus aplicaciones.

Y descargue el archivo EXE para Windows e indique dónde instalar OpenCV (se recomienda una ruta
corta como C:\OpenCV, donde C:\ es una unidad de disco duro).

2. (Opcional) Instale Intel TBB para permitir la paralelización del código en OpenCV. No utilizaremos más
OpenMP. Es importante que tome nota acerca de que sólo la versión TBB 2.2 o posterior funcionarán.
Para los usuarios de Ubuntu 9.10, pueden hacerse con los paquetes libtbb2 y libtbb-dev desde Ubuntu
10.04 (Lucid Lynx), que permitirán instalar y funcionar sin problemas sobre la 9.10 (Hoy en día Intel TBB
no es gratuito, al menos durante el periodo de 30 días de evaluación).

3. Vea el documento "SPD BASIC CONFIGURATION OF WINDOWS AND VS2010 FOR OPENCV" para
configurar el sistema operativo y Visual Studio 2010.

Cómo ejecutar Visual Studio 2010 ya preconfigurado con OpenCV.

pp.bat
set OPENCV_DIR=L:\Opencv-2.4.9\build
set OPENCV_VERSION=249
set path=%path%;%OPENCV_DIR%\x86\vc10\bin;%OPENCV_DIR%\common\tbb\ia32\vc10
"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\devenv.exe"

295
Configuración básica de Windows y Visual Studio 2010 para OpenCV.

Este documento describe cómo configurar un nuevo proyecto VS2010 (Visual Studio 2010 de Microsoft)
utilizando las librerías OpenCV. Además se describen otras configuraciones interesantes.

Importante: Tenga en cuenta que VS2010 no debe ser lanzado hasta que estas variables hayan sido definidas, ya
que las tareas o procesos de VS2010 necesitará conocer dichas variables.

Configuración del sistema operativo Windows.

1) Declare estas dos variables de entorno del sistema en el Panel de Control (esto es, botón derecho sobre
el icono de “Mi PC” o “Equipo”, busque la opción “Configuración avanzada del sistema” → “Opciones
avanzadas” → “Variables de entorno” → “Variables del sistema” → “Nueva”; o bien, “Botón de inicio” →
“Panel de control” → “Propiedades del sistema” → “Variables de entorno” → “Variables del sistema” →
“Nueva”. Si no dispone de privilegios suficientes para declarar las variables de sistema, puede declarar
“variables de usuario”. Tal como sigue, se supone que C:\ es la unidad donde está ubicado OpenCV:
OPENCV_DIR → C:\opencv\build

OPENCV_VERSION → 242 (por ejemplo para la versión OpenCV 2.4.2)

2) Igualmente se necesitan algunos DLL (como las TBB de Intel), por lo que deben ser añadidas a la variable
PATH (similar a MSDOS) y en la misma ventana de configuración (esto es, “Variables de entorno del
sistema”), de esta forma:

Añada un punto y coma (;) y después:


%OPENCV_DIR%\x86\vc10\bin;%OPENCV_DIR%\common\tbb\ia32\vc10

De esta forma los DLL de TBB podrán funcionar (hoy en día Intel TBB no es gratuito, al menos durante el
periodo de 30 días de evaluación).

Nota: Los espacios no están permitidos en la variable PATH.

Haga clic sobre “OK” y cierre el “Panel de control”.

3) Los siguientes cambios evitan archivos grandes en la ruta del código:

La opción “Tools (or Deug)” → “Options and Stettings” → “Text editor” → “C/C++” → “Advanced” →
“Always use fallback location” deberá marcarse como “TRUE”. Hacemos referencia a que siempre use la
ubicación de recursos de reserva.

La opción “Tools (or Deug)” → “Options and Stettings” → “Text editor” → “C/C++” → “Advanced” →
“fallback location” deberá indicarse un directorio temporal, por ejemplo “C:\temp_vs2010”. Hacemos
referencia a que a la ubicación de recursos de reserva.

Los valores de estos dos parámetros de configuración desde “Proyect” → “Properties” → “Configuration
properties”:

Output directory → $(SoluctionDir)$(Configuration)\

Intemediate directory: → $(Configuration)\

296
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Para hacer esto, declare una nueva variable de entorno del sistema en el Panel de Control (esto es, botón
derecho sobre el icono de “Mi PC” o “Equipo”, busque la opción “Configuración avanzada del sistema”
→ “Opciones avanzadas” → “Variables de entorno” → “Variables del sistema” → “Nueva”; o bien, “Botón
de inicio” → “Panel de control” → “Propiedades del sistema” → “Variables de entorno” → “Variables del
sistema” → “Nueva”. Si no dispone de privilegios suficientes para declarar las variables de sistema, puede
declarar “variables de usuario”. Esta variable es referente a los archivos temporales de VS.

VS10_TEMP_DIR → c:\temp_vs2010

Entonces cambie ambos parámetros de configuración en la ruta:

$(VS10_TEMP_DIR)\$(ProjectName)$(Configuration)\

Esto puede conllevar algunos “efectos colaterales” por ejemplo los ficheros de código declarados ahora
en VS2010 serán ficheros genéricos en vez de códigos fuente, ficheros de inclusión, cabeceras, etc. Y por
supuesto, toda la información de debug ya no está en la ruta del fichero fuente. Para cambiar los ficheros
fuente, simplemente excluya los ficheros que nos sean de este proyecto e incluya los nuevos ficheros.

Realizado los cambios anteriores, “Properties of Solution” y “Proyect” han cambiados para soportar
ficheros grandes para incluirlos en la misma ruta donde están los ficheros importantes como (.c, .cpp, .h,
.prj, .sln y algunos más).

Importante: Tenga en cuenta que VS2010 no debe ser lanzado hasta que estas variables hayan sido
definidas, ya que las tareas o procesos de VS2010 necesitará conocer dichas variables.

Estos cambios introducidos en un proyecto, lo que ahora difiere de la típica “Console Application” que
crea VS2010.

297
Configuración de VS2010

Ahora describiremos el enfoque del uso de las librerías estáticas que tienen extensiones lib.

Recuerde que existen diferentes paquetes de reglas para cada uno de los modos build (por ejemplo, para los
modos DEBUG y RELEASE). Lo siguiente debe hacerse para cada modo. Para hacer esto, en para parte superior
izquierda de la ventana “Property Pages”, deberá seleccionar cada configuración. Existe una especial llamada “all
configurations”. Utilice esta para cambiar los dos puntos anteriores.

4) Mire dentro de las propiedades del proyecto en “C/C++” → “General” → ítem


“AdditionalIncludeDirectories” y añada las siguientes rutas:
$(OPENCV_DIR)\include; $(OPENCV_DIR)\include\opencv;%(AdditionalIncludeDirectories)

O bien, su equivalente:
%OPENCV_DIR%\include;%OPENCV_DIR%\include\opencv;%(AdditionalIncludeDirectories)

298
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

5) Mire dentro de las propiedades del proyecto en “Linker” → “Input” ítem “AdditionalDependencies” y
añada las siguientes rutas (para que pueda utilizarse Debug):
$(OPENCV_DIR)\x86\vc10\lib\opencv_core%OPENCV_VERSION%d.lib;$(OPENCV_DIR)\x86\vc10\li
b\opencv_highgui%OPENCV_VERSION%d.lib;$(OPENCV_DIR)\x86\vc10\lib\opencv_video%OPENCV_
VERSION%d.lib;$(OPENCV_DIR)\x86\vc10\lib\opencv_ml%OPENCV_VERSION%d.lib;$(OPENCV_DIR)
\x86\vc10\lib\opencv_legacy%OPENCV_VERSION%d.lib;$(OPENCV_DIR)\x86\vc10\lib\opencv_im
gproc%OPENCV_VERSION%d.lib;%(AdditionalDependencies)

O bien, su equivalente:
%OPENCV_DIR%\x86\vc10\lib\opencv_core%OPENCV_VERSION%d.lib;%OPENCV_DIR%\x86\vc10\lib\
opencv_highgui%OPENCV_VERSION%d.lib;%OPENCV_DIR%\x86\vc10\lib\opencv_video%OPENCV_VER
SION%d.lib;%OPENCV_DIR%\x86\vc10\lib\opencv_ml%OPENCV_VERSION%d.lib;%OPENCV_DIR%\x86\
vc10\lib\opencv_legacy%OPENCV_VERSION%d.lib;%OPENCV_DIR%\x86\vc10\lib\opencv_imgproc%
OPENCV_VERSION%d.lib;%(AdditionalDependencies)

Si desea utilizar una construcción modo Release, en vez de modo Debug, dejar la última letra d, es decir:
%OPENCV_DIR%\x86\vc10\lib\opencv_core%OPENCV_VERSION%.lib;%OPENCV_DIR%\x86\vc10\lib\o
pencv_highgui%OPENCV_VERSION%.lib;%OPENCV_DIR%\x86\vc10\lib\opencv_video%OPENCV_VERSI
ON%.lib;%OPENCV_DIR%\x86\vc10\lib\opencv_ml%OPENCV_VERSION%.lib;%OPENCV_DIR%\x86\vc10
\lib\opencv_legacy%OPENCV_VERSION%.lib;%OPENCV_DIR%\x86\vc10\lib\opencv_imgproc%OPENC
V_VERSION%.lib;%(AdditionalDependencies)

6) Compruebe si el desensamblador está activado. Abra “Depurar” → “Opciones y configuración” y marque


“Habilitar la depuración a nivel de dirección” (observe la elipses rojas).

299
300
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

Exámenes.

2009 Junio 05

TEORÍA BÁSICA (cada pregunta de esta sección vale 0’2 puntos)

1. Indicar el tipo de disposición lógica de la memoria (espacio de direcciones) a los que estarían orientados
los siguientes modelos de programación:

Multi-thread: Espacio de direcciones compartido


Multiproceso: Espacio de direcciones distribuido

2. Indicar la forma de comunicación (entre hilos o procesos respectivamente) más eficiente en los
siguientes modelos de programación:

Multi-thread: Variables compartidas en memoria


Multiproceso: Paso de Mensajes

3. Definir en qué consiste el paralelismo de datos.

Igual operación sobre diferentes datos.

4. Tenemos un procesador cuya frecuencia es de 1’3 GHz, A: ¿Qué CPI tendría que tener para obtener la
ejecución de 1805’5 Millones de instrucciones por segundo? B: ¿Qué ancho de banda máximo (en
MBytes/seg) me suministraría una red con enlaces de frecuencia 1’3 GHz y ancho de 24 bits?
1300 𝑀𝐻𝑧 𝑐𝑖𝑐𝑙𝑜𝑠 24 𝑀𝐵𝑦𝑡𝑒𝑠
𝐴: 𝐶𝑃𝐼 = ′
= 0′ 72 𝐵: 𝐴𝐵 = 1300 ∙ = 3900
1805 5 𝑀𝑖𝑛𝑠𝑡𝑟𝑐𝑐 𝑖𝑛𝑡𝑟𝑢𝑐𝑐𝑖ó𝑛 8 𝑠𝑒𝑔𝑢𝑛𝑑𝑜

5. Indicar si es cierta la siguiente afirmación: OpenMP es útil para implementar aplicaciones multiproceso
en arquitecturas construidas con procesadores de varios núcleos.

Razón: OpenMP está diseñado para aplicaciones multi-hilo (multi-thread). También se da como válida,
contestar que es útil siempre que fuera una aplicación híbrida MPI- OpenMP, donde se usaría MPI para
generar los procesos, y dentro de cada MultiCore se optimizara con OpenMP (multihilo).

TEORÍA GENERAL

1. (1 punto) Pretendemos ejecutar un algoritmo sobre una matriz de tamaño 270x270, en un


multiprocesador tipo MPM de 9 nodos. La matriz se trocea en 9 submatrices cuadradas. Teniendo en
cuenta que el algoritmo implica el envío de los datos limítrofes entre las zonas, ¿Cuál sería el número de
comunicaciones a realizar, teniendo en cuenta que en cada comunicación se envía un único elemento
de la matriz? Poner la fórmula con la cual hayáis calculado el resultado.
𝑁º𝐶𝑜𝑚𝑢𝑛𝑖𝑐𝑎𝑐𝑖𝑜𝑛𝑒𝑠 = (4 ∙ 2 𝑙í𝑚𝑖𝑡𝑒𝑠 ∙ 90 𝐸𝑙𝑡𝑜𝑠)
+(4 ∙ 3 𝑙í𝑚𝑖𝑡𝑒𝑠 ∙ 90 𝐸𝑙𝑡𝑜𝑠)
+(1 ∙ 4 𝑙í𝑚𝑖𝑡𝑒𝑠 ∙ 90 𝐸𝑙𝑡𝑜𝑠)

= 2.160 𝑐𝑜𝑚𝑢𝑛𝑖𝑐𝑎𝑐𝑖𝑜𝑛𝑒𝑠

301
2. (3 puntos) El siguiente código calcula pi por el método de Montecarlo (rand devuelve un entero
pseudoaleatorio entre 0 y RAND_MAX):
for(t=0,i=0; i<IMAX; i++){
x = rand(); y=rand();
r = sqrt(x * x + y * y);
if (r < RAND_MAX) t++;
}
pi= 4 * t /IMAX;

a) En un UMA con p procesadores, ¿Cómo lo paralelizaríamos con openMP? Indicar cuantos procesos
e hilos se crearían en las distintas partes del código, si está optimizado para esta arquitectura.

b) En un clúster con C nodos, usando MPI, indicar cuántos procesos e hilos se crearían en las distintas
partes del código, si está optimizado para esta arquitectura. ¿Cuántos mensajes se transmitirían?

c) ¿Cuántos procesos e hilos tendríamos en una versión optimizada para una constelación formada por
C nodos con p procesadores cada uno?

Solución:

Aparatado a)
omp_set_num_threads(p);
#pragma omp parallel for prívate(x, y, r) reduction(+:t)
for(...){
...
}
pi = 4 * t / IMAX;
NumProcesos = 1; NumHilosXproceso = p;

Aparatado b)
NumProcesos = C ; NumHilosXproceso = 1 ;
/* Cada nodo ejecutaría IMAX/C iteraciones,
y se enviarían C-1 mensajes
(desde todos los nodos al nodo maestro, para realizar la reducción)
*/
MPI_Comm_size(..., &C);
MPI_Comm_rank(..., &myid);
Inicio = myid * IMAX / C;
Fin= Inicio + IMAX / C;
for (t=0, i=inicio; i < Fin; i++){
...
}
MPI_Reduce(&t, &total, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (myid==0) pi = 4 * total / IMAX;
...

Apartado c)

En una constelación de C nodos y p procesadores por nodo, en cada nodo habría 1 proceso y p
hilos, de forma que en total tendríamos C procesos y C*p hilos.

302
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P1. (5 puntos) Tenemos una aplicación que se va a ejecutar en un multiprocesador MPM con 16 nodos. La
aplicación consiste en lo siguiente:

1. El nodo 0 tiene almacenada una matriz de datos a procesar. Suponer que estos datos están organizados
de la forma más conveniente.
2. El nodo 0 trocea la matriz y la distribuye en partes iguales entre los procesadores.
3. Repetir 10 veces los pasos 4 a 5.
4. Los nodos impares envían sus datos a los pares y viceversa.
5. Los nodos realizan un cálculo usando sus datos locales y los recibidos de otros nodos.
6. Los nodos envían sus matrices parciales al nodo 0.

Se proponen dos posibles topologías para el sistema MPM: Hipercubo (existe un enlace entre aquellos
procesadores cuyo identificador difiere en un bit) y Anillo bidireccional (los procesadores se numeran
sucesivamente). Se pide:

a) Escribir el código MPI o Pseudocódigo indicando los envíos y recepciones bloqueante y no bloqueante.

b) Calcular el valor de 𝑙 en ambos caso e indicar que topología es más adecuada para este problema.

Solución:

Apartado a)

Suponemos que n es la dimensión de la matriz, y que (N mod num_proc) == 0.


double M[n][n], Maux[n/num_proc][n], Mveci[n][n];
int id, num_proc, num_datos, num_filas, j;
principal(){
MPI_comm_rank(MPI_COMM_WORLD, &id);
MPI_comm_size(MPI_COMM_WORLD, &num_proc);

num_datos = (n*n) / num_proc;


num_filas = n / num_proc;

MPI_Scatter(M, num_datos, MPI_DOUBLE, Maux, num_datos, MPI_DOUBLE, 0 ,MPI_COMM_WORLD);

for (i=0; i<10; i++){

if ((id % 2)==0){ // Es par


for (j=1; j<num_proc; j+=2) // Envío no bloqueante
MPI_ISend(Maux, num_datos, MPI_DOUBLE, j, "datos", MPI_COMM_WORLD);
for (j=1; j<num_proc; j+=2) // Recepción bloqueante
MPI_Recive(Mveci[j*num_filas], num_datos, MPI_DOUBLE, j, "datos", MPI_COMM_WORLD, status);
}

else{
...
// Es impar, sería igual pero cambiando en los bucles j=1 por j=0
...
}

Maux = Tratar_datos(Maux, Mveci);

MPI_Gather(Maux, num_datos, MPI_DOUBLE, M, num_datos, MPI_DOUBLE, 0 ,MPI_COMM_WORLD);


}

303
Apartado b)

Caso Hipercubo:

1101 1001 1000 1100 13 9 8 12

1111 1011 1010 1110 15 11 10 14

0101 0001 0000 0100 5 1 0 4

0111 0011 0010 0110 7 3 2 6

Con esta topología, observamos que todos los nodos pares se encuentran a un lado y todos los impares
al otro. Se trata por tanto de una red simétrica vista desde cualquier punto de vista, por lo que 𝑙 será
igual para cualquier nodo que estudiemos.

Veamos el caso de uno cualquiera de sus nodos, tomemos por ejemplo, el nodo 3 [0011] :

(𝑡𝑟𝑚 1 → 1 𝑚𝑠𝑔) + (𝑡𝑟𝑚 2 → 3 𝑚𝑠𝑔) + (𝑡𝑟𝑚 3 → 3 𝑚𝑠𝑔) + (𝑡𝑟𝑚 4 → 1 𝑚𝑠𝑔) 20


𝑙3 = 𝑙 = =
8 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑔𝑒𝑛𝑒𝑟𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 8

Caso Anillo bidireccional:

Igual que en el caso del Hipercubo, resulta una topología simétrica, por lo que el valor de 𝑙 es el mismo para
cualquier nodo. Veamos el caso de uno cualquiera de sus nodos. Tomemos por ejemplo, el nodo 0.

(𝑡𝑟𝑚 1 → 2 𝑚𝑠𝑔) + (𝑡𝑟𝑚 3 → 2 𝑚𝑠𝑔) + (𝑡𝑟𝑚 5 → 2 𝑚𝑠𝑔) + (𝑡𝑟𝑚 7 → 2 𝑚𝑠𝑔) 32


𝑙0 = 𝑙 = =
8 𝑚𝑒𝑛𝑠𝑎𝑗𝑒𝑠 𝑔𝑒𝑛𝑒𝑟𝑎𝑑𝑜𝑠 𝑝𝑜𝑟 𝑐𝑎𝑑𝑎 𝑛𝑜𝑑𝑜 8

Concluimos con que se comporta mejor en una topología tipo Hipercubo al tener un valor de 𝑙 menor.
304
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2013 Abril 22

T1. (1 punto) ¿Cuáles son las principales causas que limitan el ILP en los GPP (procesadores de propósito general)
actuales?

T2. (1’5 puntos) Enumerar y definir brevemente los distintos enfoques usados para reducir el tiempo de
ejecución en una máquina encadenada.

T3. (1’5 puntos) Enumerar y definir brevemente las técnicas de planificación estática avanzada que conozca.

P1. (1 punto) Indique cómo se escribe en lenguaje C el siguiente bucle para que sea paralelizable, si se utilizan
varios acumuladores parciales dentro del bucle.
for (i=0; i<1024; i+=2) p = p + x[i] * y[i]

P2. (5 puntos) Dado el siguiente código de alto nivel:


int i;
float x,y[100],z[100]; …
for (i=0; i<100; i++)
x += y[i] / z[i];

Suponer que la división tarda 10 ciclos y el resto de operaciones sólo 1 ciclo. Suponer BTB que siempre acierta.

b) Escribir el código (para un RISC escalar). Por comodidad usar un único registro base, modificando los
desplazamientos según el array.

c) Aplicar el Algoritmo de Tomasulo para un RISC escalar y calcular el CPI para el estacionario. Estudiar los
casos UF DIV segmentada y no segmentada.

d) Ídem para superescalar de grado 3 (donde siempre puede lanzar 4 instrucciones en la fase IF, es decir
hay una IFU que tiene siempre llena la ventana de IF).

305
2013 Junio 04

Modelo 1

T1. (4 puntos) Defina y explique en qué consiste el Grid computing.

P1. (6 puntos) Pretendemos construir un multiprocesador NUMA aprovechando 16 procesadores “antiguos”


(RISC a 600MHz). También estamos pensando en adquirir un software de optimización para ocultar la latencia,
de forma que evitase el bloqueo de los procesadores en los accesos a la red. La topología sobre la cual queremos
montar este sistema es una malla 2D cuadrada, y el patrón de comunicaciones de la aplicación es el de que cada
nodo se comunica con todos los nodos de su fila y de su columna. ¿Cuáles serían los AB mínimos de los enlaces
para que la red soportara en la bisección el AB generado por el programa, tanto en el caso de usar el software
de optimización, como en el de que no se use?

Datos: %ld/st = 25%; %Accesos Remotos = 15%; Mr = 3%; Tamaño Medio Mensaje = 100 Bytes; CPI ideal = 0’5
ciclos/instrucción; El tiempo medio de acceso a la red lo podemos aproximar por: t acc,red = 2 ∗ 𝑙 ∗ 𝑡ℎ donde
th = 20 ciclos CPU.

Modelo 2

T1. (4 puntos) Defina y explique en qué consiste el Paradigma Publicación/Suscripción.

P1. (6 puntos) Pretendemos construir un multiprocesador NUMA aprovechando 12 procesadores “antiguos”


(RISC a 800MHz). También estamos pensando en adquirir un software de optimización para ocultar la latencia,
de forma que evitase el bloqueo de los procesadores en los accesos a la red. La topología sobre la cual queremos
montar este sistema es un anillo bidireccional (numerar los nodos consecutivamente), y el patrón de
comunicaciones de la aplicación es el de que cada nodo par se comunica con todos los nodos impares, y cada
nodo impar se comunica con todos los nodos pares. ¿Cuáles serían los AB mínimos de los enlaces para que la
red soportara en la bisección el AB generado por el programa, tanto en el caso de usar el software de
optimización, como en el de que no se use?

Datos: %ld/st = 32%; %Accesos Remotos = 18%; Mr = 2’5%; Tamaño Medio Mensaje = 128 Bytes; CPI ideal = 0’5
ciclos/instrucción; El tiempo medio de acceso a la red lo podemos aproximar por: t acc,red = 2 ∗ 𝑙 ∗ 𝑡ℎ donde
th = 25 ciclos CPU.

306
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2013 Junio 18

T1. (1’5 puntos) Define los conceptos: Procesador VLIW, Ejecución fuera de orden y Planificación estática.

T2. (1’5 puntos) Hay dos tipos de sistemas P2P: Estructurados (por ejemplo, aquellos que usan el protocolo
Chord) y no estructurados (por ejemplo, los que usan inundación (flooding) o caminos aleatorios (random
walks)). Explica brevemente las diferencias y las principales ventajas/desventajas de los dos tipos.

P1 (3’5 puntos) Sea un procesador encadenado con el Algoritmo de Tomasulo clásico, donde la duración de la
UF ADDFP (totalmente segmentada) es 3 ciclos (IF IS A1 A2 A3 WB) y tiene 3 RS de tal UF e infinitos CDB. Se
ejecuta un programa que sólo contiene instrucciones ADDF y donde cada una depende de las dos anteriores (ver
código). Se pide:

a) (1’0 puntos) Dibuje el cronograma de las primeras seis instrucciones, indicando todos los bypasses y la RS
que ocuparía cada instrucción (numere las RS de 1 a 3).

b) (1’5 puntos) Para el cronograma anterior, el estado y contenido de todos los campos de cada RS al final del
ciclo 7 (contando los ciclos desde 1 para la primera IF). El nombre de las etiquetas coincide con el de las RS,
y es ADDFj, con j = 1, 2, 3.

c) (1’0 puntos) Los CPI datos, CPI estructural y CPI total en el estacionario, suponer que ejecutan infinitas instrucciones.

Código:
ADDF F1, F2, F1 ; antes del código, F1 y F2 están disponibles en el fichero de
; registros FP, y valen 1.0 y 2.0 respectivamente.
ADDF F2, F2, F1
ADDF F1, F2, F1
ADDF F2, F2, F1

P2 (3’5 puntos). Tenemos una matriz float m[n][n] en un sistema con p procesadores (podemos suponer que n
es múltiplo de p). Queremos implementar en MPI un algoritmo donde el valor final de m[i][j] depende de los
valores de sus vecinos:
m[i][j] = f(m[i-1][j], m[i+1][j], m[i][j-1], m[i][j+1])

En el caso de que algún elemento no exista (bordes) se sustituye su valor por 0. Para implementar el algoritmo
seguimos el siguiente esquema:

1. El nodo maestro (0) reparte la matriz por bloques iguales de filas consecutivas entre los procesadores
con rango consecutivo.
2. Cada procesador envía los elementos necesarios a los nodos que lo necesitan.
3. Se realizan los cálculos necesarios (se aplica la función f()).
4. Se repiten los pasos 2 y 3, K veces.
5. Se envían los resultados al maestro.

Se pide:

b) (2’0 puntos) Escribir el código empleando MPI o pseudocódigo equivalente.


c) (1’5 puntos) Suponiendo p = 16 y que podemos utilizar una estructura de malla 4x4 o de anillo
bidireccional demostrar cuál sería la opción más adecuada sin tener en cuenta los puntos 1 y 5 del
algoritmo.

Nota: La malla se enumera de izquierda a derecha y de arriba abajo (0 al 15). El anillo se enumera
consecutivamente del 0 al 15
307
2013 Septiembre 07

P1 (3 puntos). Dado el siguiente fragmento de código de alto nivel:


double im[100], im2[100], a;
int i;
for (i=0; i<99; i++)
im[i] = (a * im2[i]) / im[i];

a) Escribir el código para un DLX con bypasses y desenrollar 2 iteraciones. Los saltos se resuelven en ID.
Suponer “a” en F10. Existe una UF de suma flotante de 2 ciclos de duración, una de multiplicación de FP
de 4 ciclos, una de división de 10 ciclos y una de enteros de 1 ciclo. Todas segmentadas excepto la
división. Calcular el CPI. (Suponed que el estacionario se consigue en la segunda iteración).

b) Suponiendo ahora planificación dinámica con el Algoritmo de Tomasulo y BTB 100% efectiva, infinitas RS
y mismas latencias de UF que en el apartado anterior, calcular el CPI del bucle original (sin ningún tipo
de planificación estática) y la aceleración respecto al apartado anterior. (Suponed que el estacionario se
consigue en la segunda iteración).

P2. (3 puntos) Pretendemos estudiar cómo se adapta un sistema multiprocesador NUMA con 16 nodos
(procesadores RISC de 500 MHz) y dos topologías distintas, a un mismo patrón de comunicaciones (Todos con
Todos). La primera topología es en forma de anillo cordal cuyos nodos tienen grado 4 (cada nodo n se conecta
con el nodo n+2, y el nodo 15 con el 1). La otra topología es en forma de malla 2D. Suponiendo que la CPU se
bloquea en los accesos a la red y que el tiempo de acceso lo podemos aproximar por t acc,red ≈ 2 ∗ 𝑙 ∗ 𝑡ℎ

¿Qué topología tendría mayores prestaciones en función del ancho de bisección disponible y el demandado?

Datos: th = 12 ciclos de red; CPI base = 0’5 ciclos/instrucción, %LD/ST = 17%; %Accesos remotos = 22%; Mr = 1’5%;
tamaño de línea = 128 Bytes; tamaño de cabecera = 8 Bytes. Enlaces de frecuencia 125 MHz y 16 bits de ancho.

C1. (2 puntos) Se ejecutan en un UMA de 2 procesadores dos versiones paralelizadas de un sencillo algoritmo
(códigos A y B), sabiendo que cada hilo se lanza en un procesador distinto y que aproximadamente ambos hilos
van sincronizados. También se sabe que los procesadores tienen un buffer de precarga (cuyo tiempo de acceso
es idéntico al del caché) que carga la línea siguiente a la actualmente accedida. ¿Puede tardar uno de los dos
códigos más que el otro o ambos tendrán una duración similar? Razonar.

Código A Código B
double a[N]; double a[N];
hilo 1 hilo 2 hilo 1 hilo 2
for (i=0; i<N/2; i++) for (i= N/2; i<N; i++) for (i=0; i<N; i+=2) for (i=1; i<N; i+=2)
a[i] = a[i] * 3.1415926; a[i] = a[i] * 3.1415926; a[i] = a[i] * 3.1415926; a[i] = a[i] * 3.1415926;

C2. (2 puntos) Paradigma Publicación/Suscripción.

308
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2013 Diciembre 10

C1 (1’0 punto). Se pide paralelizar usando OpenMP el siguiente bucle, indicando qué variables son shared y
cuáles son private.
for (i = 0; i < n; i++) {
temp = a[i] / b[i];
a[i] = b[i] + temp * c[i];
}

C2. (3’0 puntos) Define y explique brevemente las características de los principales aspectos a tener en cuenta
en el diseño de los Sistemas Distribuidos.

P1. (2’5 puntos) Dado el siguiente código:


float a[1024], b[1024];
for (i=2; i<1024; i++)
a[i] = (a[i] + a[i-2]) / b[i-1];

a) Traducir a un RISC típico usando el mínimo número de instrucciones posibles (un único registro base
para todos los vectores) el siguiente bucle.

b) A continuación reordenar para que el mayor número de instrucciones posible esté detrás de DIVF

c) Para una única iteración y pensando en el data flow limit (suponemos que se puede ejecutar en paralelo
todas las operaciones posibles; es decir, que no tengan dependencia), ¿cuántos ciclos tarda en
almacenarse a[i] de la cuestión anterior desde que se empieza a leer el primer elemento de la memoria
de esa iteración (que sería a[i], a[i-2] o b[i])? Duración de las operaciones: ADDF: 4 ciclos, DIVF: 20,
LF: 3, SF: 2.

P2. (3’5 puntos) Pretendemos estudiar cómo se adapta un sistema multiprocesador NUMA con 16 nodos
(procesadores RISC de 500 MHz) y dos patrones de comunicación distintos (Adyacente 2D y Todos con Todos).
La otra topología es en forma de malla 2D. Suponiendo que la CPU se bloquea en los accesos a la red y que el
tiempo de acceso lo podemos aproximar por t acc,red ≈ 2 ∗ 𝑙 ∗ 𝑡ℎ Calcular para ambos patrones de comunicación
la relación entre el ancho de banda de bisección disponible y el demandado

Datos: th = 12 ciclos de red; CPI base = 0’5 ciclos/instrucción, %LD/ST = 17%; %Accesos remotos = 22%; Mr = 1’5%;
tamaño de línea = 128 Bytes; tamaño de cabecera = 8 Bytes. Enlaces de frecuencia 125 MHz y 16 bits de ancho.

309
2014 Abril 02

T1 (1’0 punto). Aplicar la Ley de Amdahl al siguiente caso: sea un procesador VLIW puro con p = 8 operaciones
por macroinstrucción. Se sabe que un compilador es capaz de empaquetar el 90% de operaciones
perfectamente, pero el 10% restante se quedan “sueltas”, es decir, cada operación ocupa toda una
macroinstrucción (quedando las otras 7 operaciones vacía).

T2. (1’0 punto) Dibujar un croquis de la técnica del software pipeline (encadenamiento software de iteraciones)
para el bucle SAXPY.

T3. (2’0 puntos) Responder brevemente a las siguientes cuestiones.

1. ¿Cuál es el número de macroinstrucciones en vuelo para un VLIW puro de 15 etapas, con p = 5


operaciones por macroinstrucción y que ejecuta además 2 instrucciones a la vez (es decir, es superescalar
de grado 2)?
2. ¿En qué casos es necesario un buffer de ROB (Reordenación) y por qué?
3. Un bucle paralelizable es vectorizado con sufijos ps y ejecuta 4 veces menos iteraciones que su versión
escalar. Si la memoria no supone un cuello de botella, ¿consigue 4 veces más GFLOPS que la versión
escalar? ¿y consume también 4 veces más AB? Razonar.
4. Calcular el IPC real de un superescalar de grado 5, que además es superencadenado y tiene 15 etapas
por instrucción y que tiene CPI bloqueo = 0’5.

P1. (6’0 puntos) Dado el siguiente código de alto nivel:


int i;
float s, c[N], b[N], a[N];
for (i=0; i<N; i++)
s = s + c[i] + b[i] + a[i];

Suponer que:

- Las etapas a considerar son IS EX.


- UF única de ADDF (duración 4 ciclos). Totalmente segmentada.
- El resto de operaciones sólo 1 ciclo y tienen infinitas UF.
- Suponer que BTB y cachés siempre aciertan.
- Frecuencia = 3 GHz.

Se pide:

a) Escribir el código (para un RISC escalar) sin aplicar optimización estáticas. Por comodidad usar un único
registro base, modificando los desplazamientos según el array.
b) Hallar el camino crítico (del flujo de datos) para una iteración y calcular su número de ciclos.
c) Hallar el camino crítico (del flujo de datos) para muchas iteraciones y calcular su número de
ciclos/iteración.
d) Aplicar para 2 iteraciones el algoritmo de Tomasulo para un RISC superescalar de grado 5. Calcular los
GFLOPS para el estacionario.
e) Desenrollar 3 iteraciones usando acumuladores parciales en lenguaje C y hallar la aceleración respecto
al caso anterior (para el estacionario).
f) Si los vectores fueran muy grandes, de manera que se tuviera siempre que acceder a RAM (siendo su AB
de 8 GB/s), calcular los GFLOPs por culpa de este acceso.
310
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2014 Junio 16

PRIMER BIMESTRE

T1 (1’6 puntos). Explicar los tipos de predictores de saltos que se suelen implementar en una BTB y poner un
ejemplo de código por cada predictor, en donde se vea cuántas veces acertaría o fallaría un predictor.

T2 (2’4 puntos).

a) Explicar con un ejemplo (en ensamblador) la utilidad de las Instrucciones predictivas o condicionadas.

b) Ley de localidad de los accesos a memoria.

c) Calcular los GFLOPs para esta máquina, si el 25% de las instrucciones (operaciones) son FLOP: VLIEW
puro con p = 5 slots por macroinstrucción, donde el 80% de las macroinstrucciones son empaquetadas
perfectamente (5 operaciones) por el compilador y 20% restante sólo contienen una operación por
macroinstrucción). Frecuencia = 1 GHz.

P1 (6’0 puntos). Se quiere evaluar un benchmark, donde la mayor parte del tiempo de ejecución está en estos
dos bucles:
float x=1.0, a[n], b[n], c[n]; int i;
a) for(i=0; i<N; i++) x = x * a[i] * b[i];
b) for(i=0; i<N; i++) c[i] = a[i] * b[i];

Se pide:

a) Dibujar el flujo de datos y hallar el camino crítico para muchas iteraciones de cada bucle.

b) Para el estacionario de estos bucles, dibujar un cronograma para un RISC superescalar de grado 3,
algoritmo de Tomasulo (suponer que el ensamblador usa un único registro base, y no se desenrolla).
Suponer:
- Las etapas a considerar son IS EX.
- Infinitos recursos de UF, buses CDB, suponer que BTB y cachés siempre aciertan, etc.
- Excepción: Número de RS es grande pero limitado.
- Duraciones: MULTF (duración 5 ciclos). Totalmente segmentada. El resto de operaciones sólo 1
ciclo.

c) Para el bucle a). desenrollar 3 iteraciones usando las optimizaciones que crea conveniente en lenguaje
C y hallar:
1. Número mínimo de UF de MULTF para que no haya bloqueo por esto (en el estacionario).
2. El ancho de banda de RAM para que ésta no suponga un cuello de botella (en el estacionario, y
si los vectores fueran muy grandes, de manera que se tuviera siempre que acceder a RAM).
- Frecuencia = 3GHz.

311
SEGUNDO BIMESTRE

P2 (6’0 puntos). Pretendemos estudiar cómo se adapta un patrón de comunicaciones a dos topologías distintas
de un sistema multiprocesador NUMA con 16 nodos (procesadores RISC de 2’4 GHz):

1. Patrón de comunicaciones:
a. Los procesos se comunican con los que no pertenecen ni a su fila ni a su
columna.
2. Topologías:
a. Malla 4x4.
b. Toro 2D.

Nota: Los nodos se enumeran de izquierda a derecha y de arriba abajo, comenzando por Toro 2D
el 0 (es par), es decir la primera fila los nodos 0, 1, 2 y 3, la segunda 4, 5, 6 y 7 etc.

Suponer que la CPU se bloquea en los accesos a la red y que el tiempo de acceso lo podemos aproximar por
t acc,red ≈ 2 ∗ 𝑙 ∗ 𝑡ℎ Calcular para ambas topologías el ancho de banda de bisección disponible y el demandado,
indicando en cada caso si la red soportaría el ancho de banda demandado.

Datos: th = 15 ciclos de red; CPI base = 0’5 ciclos/instrucción, %LD/ST = 19%; %Accesos remotos = 21%; Mr = 1’8%;
tamaño de línea = 256 Bytes; tamaño de cabecera = 8 Bytes. Enlaces de frecuencia 600 MHz y 16 bits de ancho.

T3 (4’0 puntos). Escribir en código MPI o pseudocódigo un programa que implemente el patrón de
comunicaciones del problema P2.

312
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

2014 Diciembre

Problema 1. Dado el siguiente código de alto nivel:


int i;
float x1[n], x2[n], x3[n];
for(i=1; i<N-1; i++)
x3[i-1] = x1[i] + x3[i] / x2[i];

Suponer que:

- Las etapas a considerar son IS EX.


- Duración de ADDF: 2 ciclos. Totalmente segmentadas.
- Duración de DIVF: 8 ciclos. No está segmentadas.
- El resto de operaciones sólo 1 ciclo.
- Suponer que BTB siempre acierta.
- Frecuencia = 2’5 GHz.

Se pide:

a) Escribir el código (para un RISC escalar) sin aplicar optimizaciones estáticas. Por comodidad usar un único
registro base, modificando los desplazamientos según el array.
b) Dibujar el cronograma para 2 iteraciones el algoritmo de Tomasulo para un RISC superescalar de grado
3.
c) Calcular los GFLOPs para el estacionario.
d) Si los vectores fueran muy grandes y se ejecutara en sólo 2 ciclos/iteración, de manera que se tuviera
siempre que acceder a RAM, calcular los GFLOPs si el AB de RAM está limitado a 3 GB/s.
e) Al desenrollar 3 iteraciones, ¿se producirá mucho, poco o ningún aumento de GFLOPs para los casos
anteriores c) d) e) en el estacionario? Razonar.

Problema 2. Pretendemos estudiar cómo se adapta un sistema multiprocesador NUMA con 16 nodos
procesadores RISC de 2500 MHz) y dos patrones de comunicación distintos, a una topología en forma de malla
2D. Los patrones de comunicación son:

1. Adyacente 2D.
2. Pares con ipares y viceversa.

Nota: Numerar los nodos empezando por el cero, de arriba abajo y de izquierda a derecha.

Suponiendo que la CPU se bloquea en los accesos a la red y que el tiempo de acceso lo podemos aproximar por
t acc,red ≈ 2 ∗ 𝑙 ∗ 𝑡ℎ Calcular para ambos patrones de comunicación la relación entre el ancho de banda de
bisección disponible y el demandado.

Datos: th = 20 ciclos de red; CPI base = 0’5 ciclos/instrucción, %LD/ST = 17%; %Accesos remotos = 35%; Mr = 1’8%;
tamaño de línea = 128 Bytes; tamaño de cabecera = 8 Bytes. Enlaces de frecuencia 500 MHz y 16 bits de ancho.

313
2015 Abril 09

GRUPO 1

T1. (1’0 puntos) Hallar el CPI de control del siguiente cronograma, si se sabe que en las primeras 9 iteraciones
acierta la BTB y falla en la número 10. El salto se resuelve en EX, y tras esta fase se busca la rama correcta en
caso de fallo de predicción.
Bucle:
IF IS EX WB
IF IS o EX WB
IF IS o o EX WB
IF IS EX WB
Salto: IF IS o o EX WB
hacia atrás
(del bucle)

T2. (1’2 puntos) Calcular la aceleración entre:

a. un superescalar de grado 5, que tiene CPIbloqueo_total = 0.4, frecuencia = 2 GHz y ejecuta un millón de
instrucciones (operaciones).

b. Un VLIW puro de p = 4 slots que tiene CPIbloqueo_memoria = 0.2, frecuencia 1.5 GHz y el compilador
consigue compactar el millón de instrucciones, con 3 instrucciones (operaciones) por macroinstrucción.

T3. (1’8 puntos) Clasificar los bucles según se puedan desenrollar, paralelizar o no. Poner ejemplos en lenguaje
C o similar.

Problema. (6 puntos) Dado el siguiente código de alto nivel:


int i;
float s, c[N] ,b[N]; // vectores consecutivos en memoria que caben en L1
for (i=0; i<N; i++)
s = (s * c[i]) + b[i];

Suponer que:

- Las etapas a considerar son IS EX WB (la fase IF siempre tiene preparadas suficientes instrucciones para
intentar emitir).
- Superescalar de grado m = 4.
- Frecuencia = 3 GHz
- Suponer que la fase EX de Load/Store tarda 1 ciclo. Hay un único puerto para caché L1 (una U.F.).
- El resto de operaciones con fase EX de sólo 1 ciclo y tienen infinitas U.F.
- Suponer que BTB y cachés siempre aciertan (mientras no se indique lo contrario).

Se pide:

a) Hallar el camino crítico (del flujo de datos) para muchas iteraciones y calcular su número de ciclos/iteración
b) Desenrollar 2 iteraciones usando multiplicadores parciales en lenguaje C.
c) Escribir el código (para un RISC) sin aplicar optimizaciones estáticas. Por comodidad usar un único registro
base, modificando los desplazamientos según el array.
d) Dibujar el cronograma para 2 iteraciones del algoritmo de Tomasulo.
e) Calcular los GFLOPS para el estacionario (suponiendo que el número de R.S. es finito)
314
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

GRUPO 2

T1. (1’4 punto) Realizar un croquis para poder aplicar la Ley de Amdahl y para hallar la aceleración a este caso:
- Suponemos que el 70% de un programa se puede ejecutar en paralelo en 4 procesadores,
- el 20% en 2 procesadores
- el 10% en secuencial (en un procesador).

T2. (1’0 puntos) Hallar la aceleración para la ejecución de un millón de instrucciones para un superescalar
dinámico de grado 5, que tiene CPIbloqueo_estructural (por agotamiento de R.S.) = 0.3 cic/instr, frecuencia = 3
GHz , comparando estos dos casos:
- BTB perfecta (acierta siempre).
- BTB realista: falla el 5% de las predicciones, en cada fallo introduce 20 ciclos de bloqueo de media y hay
un 20 % de instrucciones de salto.

T3. (1’6 puntos) Dibujar un croquis de la técnica del software pipeline (encadenamiento software de iteraciones)
para el bucle siguiente. Explicar las diversas partes del croquis resultante. Esbozar el código ensamblador que
resultaría (no es necesario especificar los registros de las instrucciones de overhead).
for (i=0; i<1024; i++)
x[i] = x[i] * y[i] * z[i];

Problema. (6 puntos) Dado el siguiente código de alto nivel:


int i;
double s[N], c[N] ,b[N]; // vectores consecutivos en memoria que caben en L1
for (i=0; i<N; i++) {
s[i] = s[i] * c[i] ;
b[i] = 2.0 * b[i] * c[i] ;
}

Suponer que:

- Las etapas a considerar son IS EX WB (la fase IF siempre tiene preparadas suficientes instrucciones para
intentar emitir).
- Superescalar de grado m = 5.
- Frecuencia = 4 GHz
- Suponer que la fase EX de la multiplicación tarda 3 ciclos. Es U.F. única y está segmentada.
- Suponer que la fase EX de los accesos a caché tarda 1 ciclo. Es de puerto único (U.F. única).
- El resto de operaciones con fase EX de sólo 1 ciclo y tienen infinitas U.F.
- Suponer que BTB y cachés siempre aciertan (mientras no se indique lo contrario).

Se pide:

a) Hallar el camino crítico teórico (del flujo de datos) para muchas iteraciones y calcular su número de
ciclos/iteración
b) Escribir el código (para un RISC) sin aplicar optimizaciones estáticas. Por comodidad usar un único registro
base, modificando los desplazamientos según el array.
c) Dibujar el cronograma para 2 iteraciones del algoritmo de Tomasulo.
d) Calcular los GIPS (Giga instrucciones por segundo) para el estacionario (suponiendo que el número de R.S.
es finito) si los vectores caben en L1
e) Calcular los GFLOPS para el estacionario si los accesos a los vectores fueran a RAM (suponiendo que el AB
RAM es 6 GB)
315
2015 Mayo 28

MODELO 1

C1. (1’5 Puntos)

Se va a escribir un programa para sumar un vector de 65536 (216) número según el algoritmo visto en clase:
primero se suman trozos iguales en cada procesador (o U.F.) y luego, con los acumuladores parciales obtenidos
en cada procesador, se sigue la técnica de “divide y vencerás” entre procesadores (se suma una mitad con la otra
y así sucesivamente hasta quedar un solo acumulador). ¿Qué diferencias habría en el código escrito para un
UMA bus común, para un NUMA con árbol binario y para un multicomputador con árbol binario?

C2. (1’5 Puntos)

Defina y explique brevemente tres aspectos Se va a escribir un programa para sumar un vector de 65536 (216)
número según el algoritmo visto en clase:

P1. (7 Puntos)

Pretendemos estudiar cómo se adapta un sistema multiprocesador NUMA con 16 nodos (procesadores RISC de
1500 MHz) y con topología en forma de Hipercubo, a dos patrones de comunicación distintos. El primero es un
patrón adyacente, teniendo en cuenta que todos los nodos se comunican con aquellos con los cuales tiene
conexión directa. Por otra parte, tenemos un patrón de comunicaciones del tipo Todos con Todos. Suponiendo
que la CPU se bloquea en los accesos a la red y que el tiempo de acceso lo podemos aproximar por t acc,red ≈
2 ∗ 𝑙 ∗ 𝑡ℎ ¿Soportaría la red en su bisectriz el tráfico generado por ambos patrones de comunicación?

Datos: th = 15 ciclos de red; CPI base = 0’4 ciclos/instrucción, %LD/ST = 26%; %Accesos remotos = 34%; Mr = 2’4%;
tamaño de línea = 128 Bytes; tamaño de cabecera = 8 Bytes. Enlaces de frecuencia 300 MHz y 24 bits de ancho.

Nota: La red hipercubo se construye uniendo los enlaces cuya numeración binaria difiere en un bit.

316
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

MODELO 2

C1. (1’5 Puntos)


𝑜𝑟𝑑𝑒𝑛 𝑑𝑒 𝑐𝑜𝑚𝑝𝑢𝑡𝑎𝑐𝑖ó𝑛 √𝑛
Si en una aplicación paralelizable en p procesadores se cumple que se su = 𝑂 ( ),
𝑜𝑟𝑑𝑒𝑛 𝑑𝑒 𝑐𝑜𝑚𝑢𝑛𝑖𝑐𝑎𝑐𝑖ó𝑛 √𝑝
entonces no es escalable respecto al número de procesadores (n = número de datos de la aplicación). Diga si
esta afirmación es verdadera o falsa, y razone la respuesta.

C2. (1’5 Puntos)

Explique brevemente los tipos de Arquitecturas Centralizadas con las que nos podemos encontrar. Ponga algunos
ejemplos.

P1. (7 Puntos)

Pretendemos ejecutar un algoritmo sobre una matriz de tamaño 1024x1024, en un multiprocesador tipo MPM
de 16 nodos. La matriz se trocea en 16 submatrices. Teniendo en cuenta que el algoritmo implica el envío de los
datos limítrofes entre las zonas,

a) (1 punto) Proponed una división de la matriz que minimice el número de comunicaciones.

b) (2’5 puntos) ¿Cuál sería el número de comunicaciones a realizar, teniendo en cuenta que en cada
comunicación se envía un único elemento de la matriz? Poned la fórmula con la cual hayáis calculado el
resultado.

c) (3’5 puntos) Escribir un programa en MPI o pseudocódigo que implemente dicho algoritmo.

317
2015 Junio 11

Primer bimestre:

T1. (2’0 puntos)

a) Desenrolle 4 iteraciones del siguiente bucle de la forma más eficiente que sepa.
b) ¿Qué cantidad de cargas de memoria, de multiplicadores y de sumadores en paralelo se necesitan para
poder ejecutarlo en forma de árbol binario (utilizando la reasociación de sumas)?
for (f=0 ; f< m ; f++)
suma += (x[f] * y[f]);

T2. (2’0 puntos) Conteste brevemente:

a) ¿Por qué interesa medir el rendimiento con una colección de programas?


b) ¿Qué propiedad tienen las medias geométricas para calcular la aceleración para una colección de
programas entre dos máquinas?
c) ¿Cuáles son las dos principales causas de bloqueo en procesadores GPP actualmente?
d) ¿A qué se reduce la aceleración de un programa entre dos máquinas compatibles y de la misma
frecuencia de reloj?

P1. (5’0 punto). Se quiere evaluar un benchmark, donde la mayor parte del tiempo de ejecución está en este
bucle.
float s[N], a[N][3]; int i;
for (i=0 ; i<N ; i++)
s[i] = a[i][0] +a[i][1] + a[i][2];

Suponer:

- Las etapas a considerar son IS EX.


- Suponer que BTB y cachés siempre aciertan, etc. De forma que se tienen siempre 6 instrucciones
cargadas para poder emitirlas.
- Infinitos recursos de UF, buses CDB, excepto de la unidad funcional de acceso a caché (sólo hay 1) y de
suma (sólo hay 1).
- Excepción: número de R.S. es grande pero limitado.
- Frecuencia = 3 GHz
- Duraciones: ADDF (duración 2 ciclos). Totalmente segmentada. El resto de operaciones sólo 1 ciclo.

Se pide:

a) Para la primera iteración, dibujar un cronograma para un RISC superescalar de grado 6, algoritmo de
Tomasulo (suponer que el ensamblador usa un único registro base, y no se desenrolla). No es necesaria
una traducción a ensamblador detallada.
b) Para el estacionario, dibujar un cronograma (para la misma máquina) sólo para las instrucciones de
acceso a caché y de suma, donde se demuestre si van a existir bloqueos.
c) En el estacionario, ¿cuál debería ser el número de unidades funcionales para que no exista bloqueo
nunca?
d) En el estacionario, ¿cuál debería ser el Ancho de Banda de RAM para que no exista bloqueo nunca (aquí
suponemos que la matriz está en memoria)?
318
Miguel Ángel Cifredo
macifredo@gmail.com

Arquitectura de Sistemas Distribuidos

P2. (1’0 punto). Dibujar en un grafo el camino crítico para muchas iteraciones del bucle (cálculo de la serie de
Fibonacci). Duración Load/Store: 3 ciclos, Suma: 2 ciclos. Resto operaciones 1 ciclo.
serie[0]=1; serie[1]=1;
for (f=2 ; f< m ; f++)
serie[f] = serie[f-1] + serie[f-2];

Segundo bimestre:

T3. (1’5 puntos). Indicar, en OpenMP, la directiva para paralelizar el siguiente código. Indicar que variables deben
ser privadas y cuáles compartidas.
for(i=0; i<10000; i++)
for(j=0; j<1000; j++)
{ b = 2.0 * i;
a+= sqrt(b + j);}

T4. (0’75 puntos) Responda Verdadero o Falso y explique brevemente la razón.

Una de las limitaciones principales de las Arquitecturas Multicapa 2-tier es su alto acoplamiento del cliente
respecto al servidor.

V/F:

T5. (0’75 puntos) Responda Verdadero o Falso y explique brevemente la razón.

En las arquitecturas descentralizadas los componentes actúan como clientes o como servidores, pero nunca
como ambos al mismo tiempo.

V/F:

319
P3 Tenemos una aplicación que se va a ejecutar en un multiprocesador MPM con 16 nodos. Los nodos están
organizados con forma de malla 2D (4x4, se numera empezando por el 0, de izquierda-derecha y de arriba-abajo).
La aplicación consiste en lo siguiente:

1. El nodo 0 tiene almacenada un vector de datos a procesar de tamaño n.


2. El nodo 0 trocea el vector y lo distribuye en partes iguales entre todos los procesadores (incluido él
mismo).
3. Repetir 10 veces los pasos 4 y 5
4. Los nodos envían sus datos a los nodos que se encuentran en una fila o columna distinta a la suya.
5. Los nodos realizan un cálculo usando sus datos locales y los recibidos de otros nodos.
6. Los nodos envían sus vectores parciales al nodo 0, el cual muestra los resultados.

Se pide:

a) (3’5 puntos) Escribir el código MPI que implemente este algoritmo (ver nota).

Teniendo en cuenta sólo el punto 4 del algoritmo, y que cada envío de los datos de un nodo a otro se
considera un único mensaje (no se tendrá en cuenta el envío inicial, ni el envío final de los datos por
parte del nodo 0)

b) (1’5 puntos) Calcular el valor de 𝑙 ̅ para la malla 2D.

c) (2’0 puntos) Calcular el porcentaje medio de mensajes en la bisectriz para la malla 2D y también para el
caso de ejecutar el mismo programa en un anillo bidireccional (los procesadores se numeran en el anillo
sucesivamente 0,1,2,...).

Nota: Usar exclusivamente las siguientes funciones MPI:


MPI_Init(&argc, &argv);

int MPI_Comm_size (MPI_COMM_WORLD, int *totTasks);

Devuelve el número de procesos en el grupo (comunicador MPI) en totTasks.


int MPI_Comm_rank(MPI_COMM_WORLD, int *task_id);

Devuelve el identificador del proceso actual (de 0 a totTasks-1) en *task_id


int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);

– Envío básico bloqueante.


– La rutina retorna cuando el buffer de la aplicación está libre
int MPI_Recv(void *rbuf, int count, MPI_Datatype datatype, int source,int tag, MPI_Comm comm,
MPI_Status *status);

– Recibe un mensaje (bloqueante)


MPI_Finalize();

320

También podría gustarte