Computación Paralela Científica
Computación Paralela Científica
Computación Paralela Científica
e Ingeniería
Grado: Licenciatura en
Computación
Matrícula: 96319369
Firma: __________________________________________
Agradecimientos............................................................................................................iv
Prefacio...........................................................................................................................vi
1. Introducción
1.1 Cómputo Científico ............................................................................1
1.2 Importancia de los métodos numéricos ...........................................3
1.3 Problemas de gran reto.......................................................................4
1.4 El rol de las supercomputadoras .......................................................5
1.5 Estado actual del cómputo de alto rendimiento..............................6
2. Arquitectura de Supercomputadoras
2.1 Introducción ........................................................................................9
2.2 Clasificación de las computadoras...................................................11
2.3 Multiplicación pipelined ...................................................................15
2.4 Redes de interconexión ....................................................................17
4. Métricas de rendimiento
4.1 Introducción ......................................................................................45
4.2 Algoritmia: Análisis asintótico .........................................................47
4.3 Rendimiento de computadoras paralelas (Un primer
acercamiento).....................................................................................50
4.4 Ley de Amdahl ..................................................................................51
4.5 Aceleración superlineal.....................................................................54
4.6 Métodos más realistas.......................................................................55
4.7 Eficiencia............................................................................................58
4.8 Datos empíricos ................................................................................58
4.9 Entradas/Salidas ...............................................................................60
5. ¿Cómo optimizar un código?
5.1 Introducción ......................................................................................63
5.2 Aspectos a considerar el punto flotante .........................................64
5.3 Opciones de optimización en la Origin 2000 ................................65
5.4 Sugerencias en la optimización en la Origin 2000.........................69
6. Un ejemplo: DFT++
6.1 Introducción ......................................................................................71
6.2 DFT++:Primeros resultados...........................................................74
6.3 Conclusiones......................................................................................77
a) Apéndices:
AàLa Origin 2000 y los procesadores MIPS R10000...............................79
BàTécnicas de optimización (Las reglas de Bentley)................................83
CàReferencia rápida. ....................................................................................92
ii
iii
AGRADECIMIENTOS
El autor desea dar las gracias al Dr. Marcelo Galván Espinosa por su
paciencia y apoyo esperando que este trabajo llene todas las expectativas y
más. También al Dr. Carlos Amador Bedolla, al Dr. Roberto Amador (sin
parentesco entre ellosJ), a mi compadre Edgar Efrén Hernández Prado y a
mi familia (principalmente Estela Carrera Martínez) por sus apoyos en los
tiempos más difíciles, a mis amigos que me apoyaron durante la carrera
(Ernesto, Grisel Trani, Arlette Violeta Richaud), a mis profesores por
aguantarme y principalmente a mis padres(†) a quienes les dedico el siguiente
pensamiento:
Con la mayor gratitud por los esfuerzos realizados para que lograra terminar
mis Carreras Profesionales, siendo para mi una de las mejores herencias. A
quienes me han heredado el tesoro más valioso que pueda dársele a un ser:
Amor. A quienes sin escatimar esfuerzo alguno han sacrificado gran parte de
su vida para formarme y educarme. A quienes la ilusión de su vida ha sido
convertirme en una persona de provecho. A quienes nunca podré pagar
todos sus desvelos y sacrificios ni aún con las riquezas más grandes del
mundo y a quienes son un modelo a seguir
iv
v
PREFACIO
El cómputo científico es una de las áreas más nuevas de la ciencia así como de
mayor desarrollo e ímpetu. El siglo XXI se dice que es el siglo de la
bioinformática y sin el desarrollo de las supercomputadoras, éste sería
imposible. Nos encontramos en un círculo virtuoso donde los científicos
quieren un mayor poder de cómputo provocando que las ciencias de la
computación se desarrollen y con esto, se desarrollan las ciencias que se
apoyan en las computadoras.
Por último, veremos un ejemplo que es muy interesante ya que está escrito en
C++, algo inusual en el cómputo científico actual; pero que en un futuro
veremos en un mayor desarrollo y que nos trae un ejemplo de la naturaleza de
los problemas actuales del cómputo numérico intensivo. Los apéndices nos
dan información importante para conocer la arquitectura de la máquina
Origin 2000 en la que trabajamos (Berenice-UNAM), así como de las técnicas
de optimización más importantes.
vi
Capítulo 1
INTRODUCCIÓN
1. Las simulaciones por computadora por lo regular son mucho más baratas y rápidas que los
experimentos físicos.
2. Las computadoras pueden resolver un margen mucho más amplio de problemas que los que
podrían resolverse con equipos de laboratorio específicos.
Los científicos teóricos y experimentales son usuarios de los grandes códigos de programas
suministrados por los científicos computacionales. Los códigos deben generar resultados precisos con
mínimo esfuerzo por parte del usuario. Los científicos computacionales deben aplicar tecnologías
avanzadas a la modelación numérica (métodos numéricos), la ingeniería del hardware y del software así
como el desarrollo de éstos. Para usar eficientemente una computadora es necesario optimizar el
programa de aplicación de acuerdo a las características de la computadora, como una herramienta en el
alcance de metas específicas.
Algunas de las principales preguntas que surgen en la búsqueda de esta meta son:
- ¿Cómo debe ser formulado un problema científico o tecnológico de tal manera que se facilite
su tratamiento computacional?
- ¿Cómo puede el dominio de un problema ser representado mediante estructuras formales para
su procesamiento computacional?
- ¿Qué tipos de arquitecturas de computadoras son las más adecuadas para la solución de un
problema específico?
1
- ¿Qué algoritmos proveen el mejor equilibrio entre exactitud, velocidad y estabilidad
computacional?
Experimentales
(físicos, químicos,
biólogos, ingenieros)
Sugerir y Generar datos
verificar la Modelar procesos
teoría. reales, sugerir
Sugerir e experimentos,
analizar datos,
interpretar
controlar
experimentos.
aparatos.
Computacionales
Teóricos (ciencias de la
(matemáticos, computación, ingenieros
físicos, digitales, físicos,
químicos, matemáticos o químicos
lógicos) Aportar ecuaciones computacionales)
que interpreten los
resultados. Precisar
los cálculos, realizar
cálculos de gran
escala, sugerir
teorías.
Las investigaciones científicas más actuales en esta área involucran el modelado, la simulación y el
control de sistemas del mundo real. Una parte importante del cómputo científico se puede clasificar
como cómputo de alto rendimiento, es decir, gran demanda de procesamiento de datos en
procesadores, memoria y otros recursos de hardware cuya comunicación entre ellos es muy rápida Las
principales ventajas son:
- La posibilidad de poder cambiar los parámetros de una simulación para estudiar tendencias
emergentes.
2
- Estudio de sistemas en los cuales no existe una teoría exacta (métodos heurísticos,
aproximados, etc.)
Resolver una ecuación analíticamente es bastante difícil en la mayoría de los casos, a menos que la
ecuación sea extremadamente sencilla. Las dificultades encontradas al buscar soluciones analíticas se
pueden clasificar dentro de los siguientes casos:
- La ecuación es multidimensional.
Los ingenieros y científicos han optado por aproximaciones experimentales para la mayoría de los
complicados sistemas reales. Aunque, obviamente existen severas limitantes de estas aproximaciones,
tales como errores experimentales y la naturaleza burda de los resultados. Actualmente es imposible
separar la computadora del diseño y análisis de un sistema en las tecnologías más avanzadas. Los
métodos numéricos son los procedimientos matemáticos basados en operaciones aritméticas para los
cuales las computadoras calculan la solución de las ecuaciones matemáticas. Dada la naturaleza digital
de las computadoras existen varias diferencias entre los métodos numéricos y las aproximaciones
analíticas. La principal diferencia es que los espacios continuos no pueden ser representados por una
memoria principal finita, es decir, elevados requerimientos de recursos de cómputo.
Las computadoras modernas están equipadas con recursos de hardware poderosos controlados por
extensos paquetes de software. Los requerimientos actuales de procesamiento de cómputo han
generado avances tanto en la investigación como en la industria, en la microelectrónica, diseño de
procesadores avanzados, sistemas de memoria, dispositivos periféricos, canales de comunicación,
evolución de los lenguajes de programación, sofisticación de los compiladores, sistemas operativos,
entornos de programación y los retos de las aplicaciones. Traduciéndose estos avances en equipos de
cómputo adecuados para realizar cálculos a altas velocidades lográndose un círculo virtuoso entre el
avance de la ciencia que impulsa un mayor desarrollo de tecnología computacional y a la vez éste
desarrollo logra un mayor avance de la ciencia y de la tecnología.
3
1.3 Problemas de gran reto
Los problemas de gran reto son aquellos problemas que son fundamentales de la ciencia e ingeniería
con un amplio impacto económico y científico, cuyas soluciones pueden ser desarrolladas aplicando
técnicas del cómputo de alto rendimiento. Los grandes retos del cómputo de alto rendimiento son
aquellos proyectos que son demasiado difíciles para investigar aún haciendo uso de las
supercomputadoras actuales más rápidas y eficientes.
b) Problemas que debe ser resueltos en tiempo real como por ejemplo el pronóstico del clima.
- Farmacología.
- Industria automotriz.
- Modelación de superconductores.
- Sistemas biomédicos.
- Cromodinámica Cuántica.
- Exploración petrolera.
Una de las maneras en que los científicos atacan problemas muy complejos es diseñando modelos
matemáticos, que son abstracciones de fenómenos del mundo real. Estos modelos se traducen en
algoritmos numéricos y son escritos en lenguajes de alto nivel como FORTRAN, C, C++, LISP, etc.,
que permiten a los científicos visualizar vastas cantidades de datos y los han guiado a una nueva
percepción y entendimiento del mundo que nos rodea.
En los últimos años ha habido un gran avance en el desarrollo de hardware y software que permiten
por ejemplo que el poder de procesamiento de dos o más supercomputadoras estén asignadas a una
tarea común dando como resultado una nueva área en el supercómputo llamada metacómputo. El
resultado es más poder de procesamiento para atacar los problemas de gran reto.
5
1.5 Estado actual del cómputo de alto rendimiento
- Cómputo vectorial:
- Cómputo Paralelo:
- Clusters:
Una forma de poder hacer cómputo de alto rendimiento es a través de clusters o cúmulos de
computadoras, éstos son un conjunto de máquinas de arquitecturas homogéneas o
heterogéneas conectadas en red (bajo cualquier topología) para atacar problemas de cómputo
paralelo o distribuido a bajo costo. Si bien es cierto que esta forma es promisoria, no es la
panacea ya que el principal problema estriba en las conexiones vía red, que no está exenta a que
1 Pipeline se pude ver como la división de una tarea en varias subtareas cada una de las cuales puede ser ejecutada independientemente
como en una línea de producción.
2 Flopsàoperaciones de punto flotante por segundo (por sus siglas en inglés).
6
la infraestructura de la red se vea afectada por diversos factores, incluido el tipo del problema.
Pero existen importantes alcances hechos hasta ahora para incluirla como una de las más
importantes formas de hacer cómputo de alto rendimiento a un bajo costo.
- Herramientas:
Bibliografía:
7
8
Capítulo 2
ARQUITECTURA DE SUPERCOMPUTADORAS
2.1 Introducción
La eficiencia de una computadora depende directamente del tiempo requerido para ejecutar una
instrucción básica y del número de instrucciones básicas que pueden ser ejecutadas concurrentemente.
Esta eficiencia puede ser incrementada por avances en la arquitectura y por avances tecnológicos.
Avances en la arquitectura incrementan la cantidad de trabajo que se puede realizar por ciclo de
instrucción como por ejemplo el uso de memoria bit-paralela (n bits donde n es mayor que uno, son
procesados simultáneamente en oposición con bit-serial en donde solo un bit es procesador en un
momento dado), aritmética bit-paralela, memoria cache (es un buffer de alta velocidad que reduce el
tiempo efectivo de acceso a un sistema de almacenamiento (memoria, disco, CD, etc.) El caché
mantiene copia de algunos bloques de datos, los que tengan más alta probabilidad de ser accesados.
Cuando hay una solicitud de un dato que está presente en el cache, se dice que hay un hit y el cache
retorna el dato requerido. Si el dato no esta presente en el cache, la solicitud es pasada al sistema de
almacenamiento y la obtención del dato se hace más lenta dándose así por lo regular un fallo de
página), canales, memoria intercalada, múltiples unidades funcionales, lookahead de instrucciones
(consiste en buscar, decodificar y buscar los operadores de la siguiente instrucción mientras se está
ejecutando la instrucción actual) lo que da la ejecución especulativa, ejecución fuera de orden,
pipelining de instrucciones, unidades funcionales pipelined y pipelining de datos. Una vez
incorporados estos avances, mejorar la eficiencia de un procesador implica reducir el tiempo de los
ciclos que son los avances tecnológicos.
Hace un par de décadas, los microprocesadores no incluían la mayoría de los avances de arquitectura
que ya estaban presentes en las supercomputadoras (como los mencionados anteriormente). Esto ha
causado que en el último tiempo el adelanto visto en los microprocesadores haya sido
significativamente más notable que el de otros tipos de procesadores: supercomputadoras, mainframes,
etc. En la figura 2.1 se puede apreciar que el crecimiento en la eficiencia para las minicomputadoras,
mainframes y supercomputadoras ha estado por debajo del 20% por año, mientras que para los
microprocesadores ha sido de un 35% anual en promedio; mientras que en la figura 2.2 se podrá
9
observar la evolución del cómputo serial y paralelo versus el costo que han tenido observándose una
comercialización en bajo costo principalmente debido a los clusters; aunque hay que observar que el
menor costo en hardware de un cluster representa un mayor costo humano en su mantenimiento y
administración.
El tiempo para ejecutar una operación básica definitivamente depende del tiempo de los ciclos del
procesador, es decir, el tiempo para ejecutar la operación más básica. Sin embargo, estos tiempos están
10
decreciendo lentamente y parece que están alcanzando límites físicos como la velocidad de la luz. Por
lo tanto no podemos depender de procesadores más rápidos para obtener mayor eficiencia. Dadas
estas dificultades en mejorar la eficiencia de un procesador, la convergencia relativa en eficiencia entre
microprocesadores y las supercomputadoras tradicionales y el relativo bajo costo de los
microprocesadores que tienen una demanda sustancialmente mayor que la de otros procesadores que
permite dividir los costos de diseño, producción y comercialización entre más unidades, ha permitido el
desarrollo de computadoras paralelas viables comercialmente con decenas, cientos y hasta miles de
procesadores.
Este tipo de máquina se compone de un CPU que procesa las instrucciones de manera serial.
Las supercomputadoras actuales tienen más de un CPU, pero si éstos trabajan de manera
independiente con las instrucciones y con los datos, hablamos de una máquina SISD.
Estas máquinas contienen varios CPU’s los cuales trabajan en paralelo ejecutando las mismas
instrucciones sobre conjuntos diferentes de datos. Una subclase de esta categoría son las
máquinas que tienen procesadores vectoriales las cuales trabajan sobre un conjunto de datos de
manera paralela ejecutando la misma instrucción sobre cada conjunto de datos (que se
denomina vector) en uno o varios CPU’s. Este tipo de procesador contiene un conjunto de
unidades aritméticas especiales conocidas como unidades funcionales o encadenamientos
aritméticos (registros pipeline). Estas unidades son utilizadas para procesar los elementos del
vector de datos eficientemente, intercalando la ejecución de diferentes partes (etapas) de una
operación aritmética sobre elementos diferentes del vector, a medida que recorren dicha unidad
funcional. Estos procesadores contienen registros vectoriales para mantener varios elementos
(a la vez) de un vector y operar sobre ellos en un solo ciclo de reloj, en lugar de hacerlo en
varios ciclos como con las operaciones escalares. Este tipo de máquinas por lo regular son
muy caras, utilizan memoria compartida y la programación paralela es relativamente sencilla.
Posteriormente se hará un ejemplo de una operación de multiplicación pipelined.
11
3. MISD (Múltiples instrucciones, un dato):
Esta clase de máquina teóricamente procesa múltiples instrucciones sobre un mismo conjunto
de datos. Sin embargo, no existe ninguna máquina que corresponda a dicha clasificación.
Estas máquinas contienen una mayor cantidad de CPU’s, los cuales al ejecutar un proceso
pueden repartir tanto los datos como las instrucciones en los diferentes procesadores. Difiere
del SISD en que las diferentes instrucciones pueden estar relacionadas entre sí.
Existen otras clasificaciones más complejas y que son capaces de representar la mayoría de los sistemas
actuales (como la de Skillicorn); sin embargo, por su simplicidad la de Flynn es la más utilizada. Las
computadoras también pueden ser clasificadas desde diferentes puntos de vista. Por ejemplo una parte
muy importante de las computadoras, los procesadores, pueden ser clasificados como:
Sin embargo, actualmente la gran mayoría de procesadores tienen propiedades tanto de la tecnología
RISC como de la tecnología CISC como se verá en el apéndice de los procesadores. También
actualmente los procesadores son superescalares la cual es una implementación en la que las
instrucciones comunes pueden iniciar su ejecución simultáneamente y ejecutarse de manera
independiente lo cual reduce en gran medida el tiempo de ejecución de operaciones más complejas.
Otro punto de vista de clasificación de una computadora, es desde el punto de vista de la memoria las
cuales pueden ser:
12
1. Memoria compartida:
Solamente existe un espacio físico de memoria para todos los procesadores o para el único
procesador que exista en la computadora según sea el caso. En caso de que existan muchos
procesadores, todos ellos accesarán a la memoria de la misma forma. Un problema importante
para este tipo de máquinas es la escalabilidad (facilidad para incrementar el número de
procesadores y otros elementos de hardware significativamente con un correspondiente
incremento del rendimiento) debido a que el tráfico en el bus que conecta a los procesadores y
a la memoria compartida aumenta y por lo tanto baja su eficiencia
2. Memoria distribuida:
En máquinas con muchos procesadores, cada procesador tiene asociado directamente a él una
unidad de memoria (a lo cual se le conoce como nodo) y éstos están conectados a los otros
nodos mediante algún tipo de red, de manera que pueden intercambiar datos entre ellos por
medio de envío de mensajes. Su escalabilidad es mayor que en las de memoria compartida; sin
embargo la velocidad de comunicación entre los procesadores es menor (dependiente de la
eficiencia en el envío de mensajes) y este parámetros es muy importante en éstas máquinas.
Y como ocurrió en las clasificaciones anteriores, actualmente, las máquinas multiprocesadores utilizan
diferentes tecnologías para obtener un híbrido de ambas categorías, es decir, tener memoria distribuida
físicamente pero lógicamente tener memoria compartida lo cual es mas sencillo para el usuario o
programador final. También existen máquinas masivamente paralelas que dentro de un nodo de varios
procesadores utilizan memoria compartida, pero con otros nodos tienen memoria distribuida (SMP) o
en el caso del metacómputo, cada nodo sería una computadora multiprocesador unido a diferentes
computadoras conectadas por una red de gran velocidad (Gigabit), Ethernet, FDDI, ATM u otra y que
trabajan de manera conjunta. Estas máquinas tienen un costo en comparación menor a las
supercomputadoras pero la programación es mucho más complicada.
13
Entre las diferentes tecnologías utilizadas para el manejo de memoria tenemos:
14
número y dependiendo de la aplicación, el mecanismo de switches o enrutamiento se satura, es
decir, tiene poca extensibilidad.
La tecnología NUMA presenta el problema de coherencia entre los datos en las diferentes
memorias de los procesadores, ya que todos deben de estar seguros de accesar o tener los datos
actualizados que hayan sido modificados por algún otro procesador. La tecnología cc-NUMA
resuelve éste problema ya que mantiene a los procesadores al tanto de los cambios que otro
procesador pueda realizar en datos que un procesador dado esta utilizando y deba actualizar.
La conexión entre los procesadores para implementar cc-NUMA debe ser de baja latencia, es
decir, una conexión muy rápida para no tener decrementos importantes en la velocidad de
procesamiento del sistema.
Como hemos visto, en las máquinas de muchos procesadores, una parte sumamente importante de la
máquina es la red de interconexión que tenga ésta y para evaluar su eficiencia es necesario tener varios
conceptos que revisaremos posteriormente.
Para entender mejor el concepto de una unidad aritmética pipelined, a continuación se ilustra la
construcción de una unidad de multiplicación pipelined. Considere la siguiente multiplicación binaria:
15
Esta multiplicación se pude descomponer en una serie de sumas y desplazamientos como se muestra a
continuación. Los puntos representan ceros y las negritas son los valores que aparecen en el producto
final.
Una vez descompuesta la multiplicación en etapas, se puede implementar el pipeline usando sumadores
de 8 bits y compuertas AND. En la figura 2.5 muestra las primeras 2 etapas y parte de la tercera de una
unidad de multiplicación pipelined. Los registros de una etapa almacenen los resultados de la etapa
previa; además todos los valores requeridos en cada etapa son almacenados en registros. Un reloj
sincroniza las etapas indicándole a los registros cuando deben leer los resultados de una etapa y
hacerlos disponibles a la siguiente etapa. La diferencia entre una unidad pipelined y una unidad
ordinaria son básicamente los registros y el reloj, lo que permite que vayan efectuando
simultáneamente varias operaciones. El tiempo que tarda la unidad en producir el primer resultado es
el tiempo de travesía, mientras el tiempo que le toma en producir el próximo resultado es tiempo del
ciclo del reloj.
Pueden haber diferentes formas de descomponer una misma operación y la granularidad (del pipeline)
se refiere a que tan tosca sea esta descomposición. La variabilidad se refiere al número de formas en
que el mismo pipeline se puede configurar para operaciones diferentes.
16
Figura 2.5 Las etapas de una unidad de multiplicación pipelined.
En un esquema de una máquina paralela genérica, la cual es una máquina con un conjunto de
procesadores capaces de cooperar en la solución de un problema, se tienen tres componentes básicos:
- la red
Las redes de computadoras paralelas es un tópico rico e interesante porque tiene varias facetas, pero su
riqueza también las hacen difícil de entender en general. Por ejemplo, las redes de computadoras
paralelas están generalmente conectadas juntas en un patrón regular, la estructura topológica de estas
redes tiene propiedades matemáticas elegantes y hay estrechas relaciones entre estas topologías y los
patrones fundamentales de comunicación de importantes algoritmos paralelos.
17
El trabajo de una red de interconexión en una máquina paralela es transferir información de cualquier
nodo fuente a cualquier nodo destino deseado, es el soporte de las transacciones de la red que son
empleadas para realizar el modelo de programación. Debería completarse esta tarea tanto como sea
posible con una pequeña latencia y debería permitir un gran número de tales transferencias de manera
concurrente.
La red está compuesta de enlaces (links) y switches que proveen un medio para enviar la información
del nodo fuente al nodo destino. Formalmente, una red de interconexión de una máquina paralela es
una gráfica, donde los vértices V son servidores o switches conectados mediante canales de
comunicación. Un canal es una conexión física entre los servidores o los switches, incluyendo un
buffer para contener los datos que están siendo transferidos. Tiene un ancho w y una frecuencia f=1/t,
que juntos determinan el ancho de banda b=wf. Los switches conectan un número fijo de canales de
entrada a un número fijo de canales de salida; este número es llamado el grado del switch. Los
mensajes son transferidos a través de la red de un nodo fuente servidor a un nodo servidor destino a lo
largo de un camino o ruta constituida de una secuencia de canales y switches.
- El algoritmo de envío el cual determina que rutas podrían seguir los mensajes a través de la
gráfica de red. El algoritmo de envío restringe el conjunto de posibles caminos a un conjunto
más pequeño de caminos legales. Existen muchos algoritmos de envío diferentes, proveyendo
distintas garantías y ofreciendo diferentes equilibrios de rendimiento.
- La estrategia de intercambio el cual determina como los datos en un mensaje atraviesan su ruta.
Existen básicamente dos estrategias de intercambio: el intercambio de circuito en el cual el
camino de la fuente al destino es establecido y reservado hasta que el mensaje es transferido a
través del circuito y el intercambio de paquetes en el cual el mensaje es dividido en una
secuencia de paquetes. Un paquete contiene tanto información de secuencia y recorrido como
datos. Los paquetes son enviados por separado del origen al destino. Este último permite una
mejor utilización de los recursos de red porque los links y buffers están solamente ocupados
mientras un paquete los atraviesa.
- El mecanismo de control de flujo el cual determina cuando el mensaje, o partes de él, se mueve
a lo largo de su ruta. En particular, es necesario el control de flujo cuando dos o más mensajes
intentan usar el mismo recurso de red al mismo tiempo. Uno de estos flujos podría estar
18
atorado en algún lugar, llevados a los buffers, desviado a una ruta alterna o simplemente
descartado.
Entre los criterios que existen para evaluar las distintas topologías tenemos:
- el diámetro de red el cual es la longitud de la máxima ruta más corta entre cualesquiera dos
nodos, es decir, la mayor distancia entre dos nodos. La distancia de envío entre un par de
nodos es el número de conexiones atravesadas en una ruta, esta es al menos tan grande como el
camino más corto entre los nodos y puede ser más grande. Es preferible que el número de
enlaces por nodo sea una constante independiente del tamaño de la red, ya que hace más fácil
incrementar el número de nodos. También es preferible que la longitud máxima de los enlaces
sea una constante independiente del tamaño de la red, ya que hace más fácil añadir nodos. Por
lo tanto, es recomendable que la red se pueda representar tridimensionalmente. Mientras
menor sea el diámetro menor será el tiempo de comunicación entre los nodos.
- el ancho de bisección es el cual es el menor número de enlaces que deben ser removidos para
dividir la red por la mitad. Un ancho de bisección alto es preferible porque puede reducir el
tiempo de comunicación cuando el movimiento de datos es sustancial, ya que la información
puede viajar por caminos alternos y así evitar o reducir la congestión entre ciertos nodos de la
red. Igualmente un ancho de bisección alto hace el sistema más tolerante a fallas debido a que
defectos en un nodo no hacen inoperable a todo el sistema.
El tiempo para una operación de transferencia de datos es generalmente descrita mediante un modelo
linealà Tiempo de transferencia(n)=To+n/B donde n es la cantidad de datos, B es la tasa de
transferencia de los componentes de los datos en movimiento en unidades compatibles (bytes por
segundo) y el término constante To es el costo de inicio. Este conveniente modelo es empleado para
describir una colección de diversas operaciones, incluyendo mensajes, accesos a memoria,
transacciones de bus y operaciones vectoriales. Empleando este modelo, es claro que el ancho de
banda de una operación de transferencia de datos depende del tamaño de la transferencia. A medida
19
que el tamaño de la transferencia se incrementa, se aproxima a la tasa asintótica de B. Cuán rápido se
aproxime a esta tasa depende del costo de inicio.
El tiempo para transferir n bytes de información de su fuente a su destino tiene cuatro componentes
básicos, como sigueàTiempo(n)=Sobrecarga + retardo de envío + ocupación del canal + retraso de la
contención. La sobrecarga (overhead) es el tiempo que el procesador gasta en iniciar una transferencia.
Este puede ser un costo fijo, si el procesador simplemente le indica al asistente de comunicación que
inicie o puede ser lineal en n, si el procesador tiene que copiar los datos dentro del asistente de
comunicación. Es el tiempo que el procesador está ocupado con la acción de comunicar y no puede
realizar otro trabajo útil o iniciar otra comunicación durante éste tiempo.
- Arreglos lineales y anillos: la red más simple es un arreglo lineal de nodos numerados
consecutivamente y conectados por enlaces bidireccionales. El diámetros es N-1, la distancia
promedio es 2N/3 y eliminar un solo enlace parte la red, de tal manera que el ancho de
bisección es 1 enlace. Un anillo o toro de N nodos se puede formar al conectar las dos
terminaciones de un arreglo. Con enlaces unidireccionales, el diámetro es N-1 y la distancia
20
promedio es N/2, el ancho de bisección es 1 enlace y hay una ruta entre cada par de nodos.
Con enlaces bidireccionales, el diámetro es N/2, la distancia promedio N/3, el grado del nodo
es 2 y el ancho de la bisección es 2. Hay dos rutas entre cada par de nodos. Esta topología es
apropiada para un número relativamente pequeño de procesadores con una comunicación de
datos mínima.
21
Figura 2.9 Torus bidimensional
- Árboles: un árbol binario tiene grado 3. Típicamente, los árboles son empleados como redes
indirectas con los servidores como las hojas, así que para N hojas el diámetro es 2logN.
Formalmente, un árbol binario indirecto completo es una red de 2N-1 nodos organizados
como d+1=log2N+1 niveles. La distancia promedio es casi tan grande como el diámetro y las
particiones de árboles en subárboles Una virtud del árbol es la facilidad de soportar
operaciones de múltiple transmisión de un nodo a muchos como en problemas de
ordenamiento, multiplicación de matrices y algunos problemas en los que su tiempo de
solución crece exponencialmente con el tamaño del problema (NP-complejos). El esquema
básico de solución consiste en técnicas de división-y-recolección donde el problema es dividido
y cada parte se resuelve independientemente. Después se recolectan las soluciones parciales y
se ensambla la solución del problema. La división puede ser recursiva. El más serio problema
de los árboles es la bisección. Eliminar un solo enlace cercano a la raíz corta la red.
22
- Mariposas: La restricción en la raíz de un árbol puede ser prevenida si existiesen muchas raíces.
Esto es proporcionado por una red logarítmica importante llamada mariposa. Dado unos
switches de 2x2, el bloque básico de construcción de la mariposa es obtenido mediante el
simple cruzamiento de cada par de aristas. Estas mariposas de 2x2 están compuestas dentro de
una red de N=2d nodos en log2 N niveles de switches. Esta topología presenta un menor
diámetro comparado con las mallas.
- Pirámides: Estas redes intentan combinar las ventajas de las mallas y los árboles. Nótese que se
ha incrementado la tolerancia a fallas y el número de vías de comunicación sustancialmente.
23
- Hipercubo: Un hipercubo puede ser considerado como una malla con conexiones larga
adicionales, las cuales reducen el diámetro e incrementan el ancho de bisección. Un hipercubo
puede ser definido recursivamente como sigueà un hipercubo de dimensión cero es un único
procesador y un hipercubo de dimensión uno conecta dos hipercubos de dimensión cero. En
general, un hipercubo de dimensión d+1 con 2d+1 nodos, se construye conectando los
procesadores respectivos de dos hipercubos de dimensión d. Esta topología es una de las más
atractivas en cuanto al diámetro (d) y a que se escala bien. Sin embargo, dicho escalamiento se
ve impedido por el costo económico donde se utilizarían conexiones cada vez más largas y
costosas.
Actualmente se han utilizado topologías seleccionables (redes dinámicas) por el usuario o el sistema
operativo y controlada por una red de enrutamiento en donde en lugar de conectar todos los
procesadores juntos mediante enlaces directos, se conectan a una red de enrutamiento rápida que
utiliza conmutadores para establecer e interrumpir las conexiones virtuales, de una forma similar a una
red de conmutación de paquetes. Si los conmutadores se diseñan para que ocasiones retardos mínimos
en los mensajes, el retardo de comunicación se incrementará ligeramente al añadir más nodos al
sistema. Otra propiedad atractiva es que cada procesador necesita sólo una conexión bidireccional a la
red de conmutadores. Existen varios ejemplos para éste tipo de redes dinámicas como en el uso de
interruptores cruzados en una malla como el red del tipo crossbar no bloqueable el cual tiene un alto
costo de hardware, las redes omega, las redes de Benes, y la fibras de interconexión en un hipercubo
como se muestra en la siguiente figura en donde se muestran dos fibras de interconexión una con 8
ruteadores y la otra con 16. Los procesadores están conectados a los ruteadores, los cuales se
reconfiguran de acuerdo a la interconexión deseada.
24
Figura 2.14. Fibras de interconexión en una topología de hipercubo.
A continuación se presenta una tabla que resume las características de distintas redes de interconexión.
Número de Longitud de
Ancho de enlaces enlaces
Organización Nodos Diámetro bisección constante constante ¿Dinámica?
Bus o Ethernet k 1 1 Si No No
Malla Dimensión 1 K k-1 1 Sí Si No
2
Malla Dimensión 2 K 2(k-1) k Si si No
3
Malla Dimensión 3 K 3(k-1) K2 Sí Sí No
Mariposa (k+1)2k 2k 2k Sí No No
Árboles Binarios 2k -1 2(k-1) 1 Sí No No
2
Pirámide (4k -1)/3 2logk 2k Sí No No
Hipercubo 2k k 2k-1 No No No
Omega 2k (1) (2) Sí No Sí
k+1
Fibra de Interconexión 2 <k > 2k-1 (3) No Sí
(en un hipercubo)
(1) Se podría decir que este valor es k, sin embargo la interconexión es a través de switches y no de otros procesadores.
(2) No se pueden quitar enlaces y dividir la red en dos en el sentido original de la definición.
(3) El número de enlaces por procesador es constante, pero no el número de enlaces por router.
Tabla 2.1 Comparación entre los distintos tipos de redes de interconexión.
25
Bibliografía:
- Kai Hwang.
26
Capítulo 3
3.1 Introducción
El análisis y diseño de software no es una tarea trivial y es más difícil cuando se trata de programa
paralelos ya que existen una mayor cantidad de variables que se deben de tener en cuenta para un
buen análisis, diseño y mantenimiento del software creado, por lo tanto, éste es un proceso
altamente creativo y se debe de seguir una buena metodología para obtener así el software que
mejor satisfagan al máximo nuestras expectativas y/o necesidades.
Pero antes que nada, necesitamos saber las implicaciones que trae el paralelismo a un problema y
software determinado. El cómputo paralelo parece directo; aplique múltiples procesadores a un
problema para resolverlo más rápido, más realista y complejo (con más variables), más grande
(mayores datos) o con resolución más fina. Desgraciadamente, esto no ocurre así en la gran
mayoría de las ocasiones ya que el cómputo paralelo involucra una curva de aprendizaje muy
empinada, un esfuerzo intensivo del programador para pensar nuevas maneras de resolver el
problema de manera paralela lo que puede conllevar a que se escriba totalmente el código, es decir,
el código que corre de manera serial ya no nos serviría, tomar en cuenta el ambiente de ejecución
(arquitectura de la máquina paralela, balance de carga, etc). Además, las técnicas usadas para
depurar y mejorar el rendimiento de un programa serial no se extienden fácilmente en el mundo
paralelo lo que puede provocar que se trabaje meses en paralelizar una aplicación solo para
encontrar que da resultados incorrectos o que corre más lentamente.
Entonces, ¿qué debo hacer? El propósito y la naturaleza de la aplicación son los indicadores más
importantes para saber que tan exitoso puede ser la paralelización. La máquina paralela en la que se
trabaja así como el plan de ataque para la paralelización tendrán un significativo impacto en el
rendimiento del programa y en el esfuerzo que se hará para ello. En la siguiente figura se muestran
las precondiciones necesarias para saber si la paralelización es necesaria y viable.
27
Figura 3.1 Las precondiciones para el paralelismo. ¿Qué rendimiento se
necesita?
Como vemos en la figura anterior, un programa es viable para su paralelización solo si dicho
programa se utiliza muy frecuentemente, es decir, es muy productivo y necesario; por otro lado, si
un programa cambia mucho o se utiliza poco, el esfuerzo de paralelización no valdrá la pena. Si su
tiempo de ejecución es muy grande o los resultados se necesitan en muy poco tiempo (problemas
de gran reto en donde se necesitan los datos en tiempo real, como en el clima); entonces la
productividad mejorará mucho con la reducción de tiempo de ejecución al momento de paralelizar.
Por último, la resolución y complejidad actual con la que se resuelve el problema debe ser
satisfactorio para uno, si no es así; entonces el paralelismo quizá es la única solución viable para
lograr complejidad y resolución que sean satisfactorias. Si de nuestro problema vemos que es
necesario el paralelismo; entonces debemos seguir con el siguiente análisis, el cual es la naturaleza
del problema en sí.
Se han establecido las “arquitecturas del problema” los cuales son categorías que se tienen en base a
las características de las aplicaciones. Estas son:
28
Datos de la Datos de la Datos de la
etapa A etapa B etapa C
Resultados
Resultados Resultados
29
Datos de la Datos de la Datos de la
primera Cálculos segunda Cálculos tercera
etapa aplicación etapa aplicación etapa
Cálculos,
etc.
Cálculos (aplicación)
Datos finales
30
4. Paralelismo flojamente síncrono: Es éstas aplicaciones los cálculos hechos afectan
las decisiones y cálculos futuros como en el tipo de paralelismo anterior; pero
además, no existen una sincronización determinada ya que la cantidad de cómputo
necesario depende fuertemente de los valores iniciales y a la frontera del problema
y del tiempo en que se lleva a cabo la aplicación ya que pueden aparecer nuevas
variables y variar la complejidad conforme avanza el cálculo provocando muchas
veces que el avance del cálculo sea irregular e imprevisible. El paralelismo se
introduce dividiendo el trabajo entre varios procesos por cada tiempo de cómputo.
Este es el tipo de aplicación que necesitará mayor esfuerzo para su paralelización ya
que es muy importante la sincronización, el balanceo de carga y las comunicaciones
que deben de existir entre los diferentes procesos para intercambiar información
vital y continuar con el cálculo. Es muy común encontrar que un proceso produce
subprocesos para mejorar la sincronización y el balanceo de carga; lo que genera
mayor comunicación para que cada proceso pueda determinar si los valores
obtenidos pueden ser o no sobrescritos. La distribución de trabajo es lo más difícil
de lograr ya que la carga de trabajo varía espacial y temporalmente.
Desgraciadamente la mayoría de los problemas de gran reto actuales caen en el paralelismo flojamente
síncrono y es por ello, que si es necesario hacer el esfuerzo para paralelizar nuestra aplicación con éste
tipo de arquitectura de problema; tomemos en cuenta como el cómputo (así como los datos) se
comportan durante la ejecución de nuestra aplicación y con esto analizar que tipo de máquinas nos
ayudará a tener un mejor rendimiento.
31
totalmente síncronos, ésta arquitectura funciona bien; aunque existen algunos detalles; como por
ejemplo, los arreglos generalmente no tienen el mismo tamaño que CPUs por lo tanto hay que utilizar
técnicas de optimización para lograr que todos los CPUs trabajen al momento de hacer las operaciones
con los subarreglos que se generen; otro ejemplo son las operaciones condicionales donde se hacen
todas las operaciones para un arreglo; pero si en un dato de los arreglos no se cumple la condición,
entonces todo el esfuerzo se concentrará en un CPU, que será el que defina la condición. Otros
problemas son la dependencia de los datos, que puede provocar que los resultados finales sean
erróneos.
Para máquinas MIMD con memoria compartida los ciclos intensivos pueden ser paralelizados y
optimizados de tal manera que tendrán un gran rendimiento, convirtiendo éstos ciclos en una colección
de ciclos para un subconjunto de datos para cada CPU aprovechando así la memoria compartida y que
los procesos no se preocupen en las actividades de los otros CPUs; hay que seguir las técnicas de
optimización para que el compilador y uno pueda generar código optimizado en los ciclos y aprovechar
la arquitectura de la máquina. Aquí es muy importante que el programador se preocupe por la
dependencia de los datos y salvaguardar los datos compartidos para lograr así la coherencia en los
datos. En el caso del paralelismo totalmente síncrono, a veces no es tan sencillo optimizar la aplicación
para éste tipo de máquinas ya que el acceso a datos es esporádico y muy interdependientes. Sin
embargo, para aplicaciones embarazosamente paralelas y pipeline puede ser muy útil.
Para máquinas con memoria distribuida, la paralelización, depuración y optimización suele requerir un
gran esfuerzo; sin embargo, éste tipo de máquinas son las que mas se están desarrollando y mejor
costo/beneficio tienen. En éstas máquinas suelen duplicarse los datos en cada memoria del CPU que
lo requiera siendo muy importante la coherencia de los datos durante la comunicación entre los
procesos vía mensajes. En estas máquinas es muy importante tener en cuenta los problemas que
surgen con la comunicación y protección de datos compartidos (como mensajes corruptos o perdidos,
bloqueos mutuos, etc.). Además, el balance entre la velocidad de CPU (altas) y la velocidad de las
comunicaciones (relativamente bajas y costosas) es crítico y por ello, las métricas de rendimiento juegan
un papel muy importante en las aplicaciones que se ejecutan en éstas máquinas. Además, es
conveniente reducir los tiempos de comunicación (o las comunicaciones) y mejorar la sincronización
para mantener los CPUs ocupados. Las aplicaciones totalmente síncronas son impropias para éste tipo
de arquitecturas y las aplicaciones flojamente síncronas y pipeline pueden lograr un buen rendimiento si
las comunicaciones entre los procesos se dan con datos pequeños y/o lapsos de tiempo relativamente
largos entre las comunicaciones.
Para máquinas SMP (Symmetric Multiprocessor), cada nodo tiene un número par de
microprocesadores (por lo regular 4 y 8) que tienen memoria compartida, pero entre cada nodo hay
memoria distribuida. Se dice que son simétricos ya que dentro de un nodo, cada procesador puede
acceder a una locación de memoria con la misma latencia. El mejor rendimiento se obtiene por lo
regular cuando se tratan éstas máquinas como una colección de distintos sistemas de memoria
compartida en pequeña escala; es decir, aprovechar al máximo cada nodo y evitar las comunicaciones
entre los nodos. Por ejemplo generar hilos que aprovechen al máximo un nodo y varios procesos
paralelos que vean a la máquina como una arquitectura con memoria distribuida. Aplicaciones
flojamente síncronas y pipeline pueden funcionar bien en ésta arquitectura. Para lograr el mayor
32
aprovechamiento de la arquitectura de la máquina, la programación, el lenguaje y las bibliotecas
utilizadas son muy importantes.
Definitivamente, una vez que se ha analizado la arquitectura del problema y de la máquina, el lenguaje
de programación, las capacidades del compilador y las utilerías de depuración y para ver el
comportamiento del programa durante la ejecución, afectarán en gran medida el esfuerzo que se tenga
que hacer para paralelizar y optimizar la aplicación. Básicamente, existen dos modelos de
programación paralela: SPMD (multiple program, multiple data) donde se crean diferentes ejecutables
por cada CPU y SPMD (single program, multiple data) donde todas las instrucciones para todos los
CPUs son combinados desde un solo ejecutable.
Para el modelo SPMD, cada CPU ejecuta el mismo código objeto. Puede ejecutar diferentes
instrucciones en diferentes CPUs con la ayuda de una operación condicional pero por lo regular ejecuta
el mismo programa en diferentes datos. En máquinas SIMD, quizá es la única opción y para máquinas
MIMD el programador solo tiene un programa para depurar y ver su rendimiento, algo que puede ser
una ventaja. Pero puede haber un costo, todos los datos e instrucciones deber ser accedidos por todos
los CPUs eficazmente lo que aumenta considerablemente la memoria requerida y a menudo el tiempo
de acceso a la memoria se vuelve un cuello de botella para el rendimiento de la aplicación.
Para el modelo MPMD, la cual por lo regular solo aplica a máquinas MIMD, utiliza el espacio de
memoria más eficientemente y los requerimientos de espacio de código son reducidos para aplicaciones
con paralelismo pipeline y flojamente síncrono. El espacio de datos puede ser reducido cuando se
trabajan con grandes arreglos, si el programador los subdivide en porciones que sean accesibles solo
para los CPUs que los requieran. Para la depuración y optimización del rendimiento, el programador
verá los programas independientemente o como componentes de otros programas, lo que nos ayudará
con la modularidad y división del trabajo. Sin embargo, esto no siempre ayuda ya que pueden surgir
ciertos tipos de problemas para la puesta a punto del programa que son difíciles de conceptualizar; por
ejemplo, como las actividades de los CPUs independientes pueden influirse entre sí.
La gran mayoría de las máquinas paralelas, imponen el modelo SPMD debido a que sus sistemas
operativos y utilerías ven al programa paralelo como una sola entidad y no pueden reportar
información de múltiples ejecutables que se encuentren relacionados. Por lo tanto, el programador,
rara vez tiene varias opciones. Los lenguajes, librerías y utilerías por lo regular están limitadas a tipos
particulares de máquinas y quizás fabricantes. Las librerías de envío de mensajes son las más portables
pero con ello puede inhibir optimizaciones y capacidades de detecciones de errores de los
compiladores. También, la opción de los lenguajes a utilizar, deben de estar encaminados a el
compilador más robusto, a la especialización que se tenga sobre dicho lenguaje y colegas que los han
usado, a los reportes de rendimiento que se tengan sobre las bibliotecas matemáticas en la máquina ha
utilizar, etc.
33
Un aspecto importante, es que para máquinas vectoriales, el mejor rendimiento se obtiene para
aplicaciones con arquitecturas pipeline y totalmente síncronos utilizando fortran ya que éste lenguaje
maneja muy bien los arreglos. Para aplicaciones con arquitecturas flojamente síncronos puede ser más
importante ver la especialización de los programadores en un determinado lenguaje. Por lo regular, los
estudiantes de ciencias de la computación, tienen una mejor especialización en C y C++; mientras que
los investigadores manejan Fortran, una solución es que partes del programa sean escritos en Fortran
(las rutinas que tengan que ver con las bibliotecas matemáticas) y otras en C y C++ (las rutinas que
tengan que ver con el control del programa, interfeces, etc) para una mejor comunicación entre los
especialistas en la computación y los especialistas en el problema. La ingeniería de software no puede
ser olvidada para programas que se piensan paralelizar y para ver el balance entre la ingeniería de
software y la optimización no hay que olvidar que el 20% del código de las aplicaciones de problemas
de gran reto ocupan el 80% del tiempo de cómputo. Es por ello que el análisis y diseño del programa
debe ser tomada con toda seriedad.
En la metodología que se verá para el diseño de programas paralelos, durante sus dos primeras etapas,
contempla un análisis que es independiente de los aspectos específicos de la máquina en donde se
ejecutará la aplicación y en las últimas dos etapas si se tomarán en cuenta. La etapas son: partición,
comunicación, agrupación y asignación. En las dos primeras etapas nos enfocaremos en la
concurrencia y escalabilidad que mejor podamos encontrar; en las últimas dos etapas la atención
recaerá en la localidad y en el funcionamiento relacionado a la máquina. Aunque las etapas se
presentan como secuenciales, en la práctica no lo son, ya que por lo regular se dan de manera paralela y
además las últimas etapas afectan a las primeras. A continuación se hace un resumen de las cuatro
etapas:
1. Partición: El cómputo y los datos sobre los cuales se opera se descomponen en pequeñas
tareas. Se ignoran aspectos como el número de procesadores de la máquina ha usar y se
concentra la atención en encontrar oportunidades de paralelismo.
34
3.7 Diseño del programa: Partición
- Descomposición del dominio: primero se enfoca en los datos asociados al problema para
determinar su apropiada partición y finalmente se trabaja en como asociar el cómputo con los
datos. Ésta técnica es la más común. Por lo regular, primero se enfoca en las estructuras de
datos más grandes y/o los que son accesados más frecuentemente. Por lo regular, se trata de
dividir los datos en pequeños subconjuntos del mismo tamaño para mantener un balance de
carga. Cuando una operación requiere de datos de diversos procesos, entonces la
comunicación entre éstos procesos será requerida. A veces, el cómputo mismo exige que se
operen sobre diversas estructuras de datos y/o demanden diversas descomposiciones para la
misma estructura de datos, entonces se tendrá que tratar cada estructura o descomposición
separadamente y después determinar como la descomposición y el algoritmo paralelo
desarrollado para cada fase, se unan después en la única aplicación.
35
- Descomposición funcional: Es el enfoque alternativo al anterior. Primero se descomponen los
cómputos en diferentes procesos y luego se ocupa de los datos que utilizará cada proceso. Si la
división final de los datos, son disjuntos, entonces la partición habrá quedado terminado; en
caso contrario, se necesitará de comunicación para evitar la repetición de datos. Si esto sucede,
entonces se puede tratar con la técnica anterior para ver donde existen mayores posibilidades
de paralelismo. Ésta técnica es muy útil muchas veces para encontrar oportunidades de
optimización que no serían obvios utilizando solamente la técnica anterior. También suele
ayudar para la estructuración del programa a obtener ya que reduce el cómputo que será
realizado y la complejidad del código en pequeños módulos.
Por lo tanto éstas técnicas son complementarias y pueden ser aplicadas a diferentes componentes de un
problema e inclusive al mismo problema para obtener algoritmos paralelos alternativos. La fase de
partición debe de producir uno o más posibles descomposición del problema. Al particionar se deben
tener en cuenta los siguientes aspectos antes de pasar a la siguiente fase:
1- El número de tareas debe ser por lo menos un orden de magnitud superior al número de
procesadores disponibles para tener flexibilidad en las etapas siguientes.
3- Hay que tratar de que las tareas sean de tamaños equivalentes ya que facilita el balanceo de
carga de los procesadores.
4- El número de tareas o procesos deber ser proporcional al tamaño del problema. De ésta forma
el algoritmo será capaz de resolver problemas más grandes cuando se tenga más disponibilidad
de procesadores. En otras palabras, se debe tratar de que el algoritmo sea escalable; que el
grado de paralelismo aumente al menos linealmente con el tamaño del problema.
Con esto, podemos visualizar si tenemos un buen o mal diseño para la paralelización. Recordemos que
las métricas de rendimiento al final nos dirán que tan bueno fue la paralelización y en donde existen
ciertos problemas de rendimiento.
Las tareas definidas en la etapa anterior, en general, pueden correr concurrentemente pero no
independientemente. Algunos datos deben ser transferidos o compartidos entre las tareas y éste flujo
de información es especificado en la etapa de comunicación.
36
La comunicación requerida por un algoritmo puede ser definida en dos fases, primero se define que
tareas o procesos deben estar ligadas, formar un enlace, para lograr la comunicación, ya sea directa o
indirectamente, en donde una tarea necesita de los datos (consumidor) que posee otra tarea (productor)
y segundo, se especifican los mensajes que serán enviados y recibidos por dichas tareas. Recordemos
que la comunicación y el enlace genera un costo; por lo tanto es importante no introducir
comunicaciones innecesarias además de que se puede optimizar el rendimiento distribuyendo las
comunicaciones sobre muchas tareas y organizando la comunicación de una manera que permita la
ejecución concurrente.
1. Comunicación local: donde cada tarea se comunica con un conjunto pequeño de tareas (por lo
regular, sus vecinos).
2. Comunicación global: donde cada tarea se comunica con un conjunto grande de tareas (muchas
veces con todas las tareas). Útil cuando la comunicación local genera demasiadas
comunicaciones o no permite el cómputo concurrente.
3. Comunicación estructurada: donde cada tarea y sus vecinos forman una estructura regular,
como por ejemplo, un árbol.
8. Comunicación asíncrona: donde el consumidor puede obtener los datos sin la cooperación del
productor y el proceso emisor puede continuar el cómputo en forma paralela con la
comunicación. Útil cuando se utilizan estructura de datos (por lo regular buffers) distribuidos.
Todos estos tipos de comunicación o técnicas tienen sus ventajas y desventajas dependiendo de la
máquina en donde se vaya a dar y varias se pueden dar en una sola aplicación. Recordemos que el
costo de enlace y comunicación suele ser muy costoso en comparación con el tiempo de cómputo que
tiene una máquina; es por ello que es muy importante visualizar que tipo de comunicación conviene a
la máquina en donde se va a trabajar además de nunca olvidar los problemas que suelen surgir con la
37
comunicación y sincronización entre tareas. Con las métricas de rendimiento, nosotros podremos
observar que tanto afecta a nuestro rendimiento las comunicaciones y donde existen los cuellos de
botella para atacarlos y mejorar nuestro rendimiento. En ésta etapa, mínimo hay que tener los
siguientes aspectos:
b) La comunicación entre tareas debe ser tan pequeña como sea posible.
d) Los cómputos de diferentes tareas deben poder proceder concurrentemente para que el
algoritmo sea eficaz y escalable.
En la fase de partición se trató de establecer el mayor número posible de tareas con la intención de
explorar al máximo las oportunidades de paralelismo. Esto no necesariamente produce un algoritmo
eficiente ya que el costo de comunicación puede ser significativo. En la mayoría de las computadoras
paralelas la comunicación es mediante el envió de mensajes y frecuentemente hay que parar los
cómputos para enviar o recibir mensajes. Mediante la agrupación de tareas se puede reducir la cantidad
de datos a enviar y así reducir el número de mensajes y el costo de comunicación.
La aglomeración y replicación son guiadas por tres metas que a veces están en conflictos: reducir el
costo de comunicación incrementando el cómputo mediante el aumento de la granularidad y
38
reduciendo así la proporción comunicación/cómputo, muchas veces, se logra esto aumentando la
granularidad por agrupación en todas las dimensiones del problema que reduciendo las dimensiones
por descomposición; preservando la flexibilidad del algoritmo con respecto a la escalabilidad y
asignación de decisiones, logrando esto con la técnica de solapamiento de cómputo y comunicaciones
y por último reduciendo el costo del diseño e ingeniería de software ya que si se trabaja con una
pequeña parte de la aplicación que se creará; entonces hay que tomar en cuenta las particiones de los
datos y las comunicaciones que se darán con las otras partes de la aplicación para reducir los costos que
se harán para implementar toda la aplicación.
El número óptimo de tareas o procesos típicamente es determinado por una combinación del diseño
del programa con estudio empíricos (métricas de rendimiento). Por lo tanto, la flexibilidad no
necesariamente requiere que el diseño siempre cree un gran número de tareas y la granularidad puede
ser controlada en tiempo de compilación o en tiempo de ejecución mediante algunos parámetros. Lo
que es importante es no limitar innecesariamente el número de tareas que pueden ser creadas en estos
momentos.
En caso de tener distintas tareas corriendo en diferentes computadoras con memorias privadas, se debe
tratar de que la granularidad sea gruesa, es decir, que exista una cantidad de cómputo significativa antes
de tener necesidades de comunicación. Se puede tener granularidad media si la aplicación se ejecutará
sobre una máquina de memoria compartida. En estas máquinas el costo de comunicación es menor
que en las anteriores siempre y cuando el número de tareas y procesadores se mantenga dentro de
cierto rango. A medida que la granularidad decrece y el número de procesadores se incrementa, se
intensifican las necesidades de altas velocidades de comunicaciones entre los nodos. Esto hace que los
sistemas de grano fino por lo general requieran máquinas de propósito específico. Se pueden usar
máquinas de memoria compartida si el número de tareas es reducido, pero por lo general se requieren
máquinas masivamente paralelas conectadas mediante una red de alta velocidad.
2. Si se han replicado cómputos y/o datos, se debe verificar que los beneficios son superiores a
los costos y que no se comprometa la escalabilidad.
3. Se debe verificar que las tareas resultantes tengan costos de cómputo y comunicación similares.
4. Hay que revisar si el número de tareas es extensible con el tamaño del problema.
6. Analizar si es posible reducir aún más el número de tareas sin introducir desbalances de cargas,
generar altos costos de diseño de software o reducir la escalabilidad. Algoritmos que crean
39
menos tareas con granularidad grande son a menudo más simples y eficientes que aquellos que
crean muchas tareas de grano fino.
7. Si se está paralelizando un código serial, entonces los costos de las modificaciones no deben ser
muy altas para el reuso del código. En caso contrario, otras la técnicas de aglomeración para
aumentar el reuso del código.
En esta última etapa, se determina en que procesador se ejecutará cada tarea. Este problema no se
presenta en máquinas de memoria compartida tipo UMA. Estas proveen asignación dinámica de
procesos y los procesos que necesitan de una CPU están en una cola de procesos listos. Cada
procesador tiene acceso a esta cola y puede correr el próximo proceso. No consideraremos más este
caso. Para el momento actual, no hay mecanismos generales de asignación de tareas para máquinas
distribuidas, ya que es un problema NP-completo. Esto continúa siendo un problema difícil y que
debe ser atacado explícitamente a la hora de diseñar algoritmos paralelos. Nuestra meta en esta etapa,
es minimizar el tiempo de ejecución total. Se utilizan dos estrategias para lograr esto:
2. Tareas que frecuentemente se comunican con el mismo procesador para aumentar la localidad.
Claramente estas estrategias, a veces, pueden entrar en conflicto. Sin embargo, muchas veces se
utilizan ambas estrategias. Además, aquí claramente se tienen límites de recursos que restringen el
número de tareas. Para el problema de asignación, se tienen diferentes técnicas (deterministas y
heurísticas) que son eficaces para ciertas aplicaciones. De éstas, pueden ser estáticas, donde las tareas
son asignadas a un procesador al comienzo de la ejecución del algoritmo paralelo y se ejecutarán ahí
hasta el final; o dinámicas. La asignación estática en ciertos casos puede resultar en un tiempo de
ejecución menor respecto a asignaciones dinámicas y también puede reducir el costo de creación de
procesos, sincronización y terminación. En la asignación dinámica se hacen cambios en la distribución
de las tareas entre los procesadores en tiempo de ejecución, es decir, hay migración de tareas. Esto se
da con el fin de balancear la carga del sistema y reducir tiempos de espera de otros procesadores. Sin
embargo, el costo de balanceo puede ser significativo y por ende incrementar el tiempo de ejecución.
Entre los algoritmos de balanceo de carga, existen muchos heurísticos y probabilísticas los cuales son
los que tienden a tener menor costo general. Entre los algoritmos más utilizados se encuentran los
locales, los cuales no necesitan del conocimiento global del sistema de cómputo. Pero para
aplicaciones muy complejos, por lo regular se utilizan algoritmos híbridos (semi-distribuidos) los cuales
dividen los procesadores en regiones y cada uno con un algoritmo de balanceo centralizado local; pero
otro algoritmo balancea las regiones. Por lo regular, esto se utiliza si la descomposición fue de
dominio; pero si la descomposición es funcional y se saben las tareas que se van a producir y destruir
40
entonces se pueden utilizar algoritmos de planificación los cuales asignan las tareas a procesadores que
están ociosos o es probable que se encuentre en ése estado.
El balanceo puede ser iniciado por envío (cuando un procesador tiene mucha carga y envía trabajos a
otros) o por recibimiento (donde un procesador con poca carga solicita trabajo a otros). Si la carga por
procesador es baja o mediana, es mejor el balanceo iniciado por envío. Si la carga es alta se debe usar
balanceo iniciado por recibimiento. De lo contrario, en ambos casos, se puede producir una fuerte
migración innecesaria de tareas.
Entre los puntos que hay que revisar en esta etapa encontramos:
2. Si se usan algoritmos locales de balanceo, hay que asegurarse de que no sea un cuello de botella.
3. Hay que evaluar los costos de las diferentes alternativas de balanceo dinámico, en caso de que
se usen, y que su costo no sea mayor que los beneficios.
En el diseño y análisis de un programa, por lo regular éste se divide para tener pequeños módulos que
nos permiten encapsular complejos aspectos del programa, reutilizar código, etc, así como permitir
atacar el problema de tal manera que nos enfrentemos en pequeños problemas sencillos en lugar de un
problema grande y complejo, reduciendo costos y aumentando la fiabilidad del programa. Las técnicas
del diseño modular nos ayudan a lograr todo esto, creando módulos que cumplan con las siguientes
características:
- interfaz simple el cual reducirá el número de interacciones que deben ser consideradas cuando
se verifique el buen funcionamiento del sistema y facilitará el reuso de éste componente en
diferentes circunstancias
41
Debemos recordar que con un buen diseño modular se obtienen módulos bien definidos, los cuales
cada uno tienen un propósito claramente definidos y cuyas interfaces son simples y lo suficientemente
abstractos que encapsulen bien la información de tal manera que no sea necesario pensar en como se
programó el módulo para entenderlo. Obviamente no debe haber módulos que repliquen
funcionalidad y el reuso de módulos debe ser algo sencillo.
Por lo tanto, el diseño modular en la programación paralela, nos debe permitir obtener módulos que
manipulen múltiples tipos de datos y por lo tanto las estructuras de datos son las que deben de dar la
información de los datos y no la interfaz del módulo para así aumentar el reuso, en el caso de envío de
mensajes o el modelo de programación paralela SPMD o cuando los componentes del programa no se
42
pueden ejecutar concurrentemente o pueden necesitar compartir muchos datos es mejor usar la
composición secuencial, la composición coexistente se puede utilizar si los componentes se pueden
ejecutar concurrentemente, los costos de comunicación son altos y el traslape de
comunicación/cómputo es posible como se verá en el próximo capítulo y considere la composición
paralela si los costos de comunicación intracomponente son mayores a los costos de comunicación
intercomponente.
Ahora nosotros hemos completado el análisis y diseño de nuestro problema para producir uno o varios
algoritmos paralelos; pero éste no necesariamente es el definitivo y no se tiene que empezar la escritura
del código ya que varias fases aún pueden sufrir cambios dependiendo de las métricas de medición que
se verán en el siguiente capitulo los cuales nos dirán que algoritmos pueden ser los mejores, además de
pensar en el costo de la implementación, en la reutilización de código y en como los algoritmos pueden
funcionar para sistemas de cómputo aún mas grandes. Todo esto se planteará en el siguiente capitulo.
Bibliografía:
- Foster, I. Designing and Buildind Parallel Program: Concepts and Tools for Parallel Software
Engineering. Addison-Wesley, New York, 1995. http://www.mcs.anl.gov/dbpp
- Is Parallelism For You? Rules-of-Thumb for Computational Scientists and Engineers. Cherri
M. Pancake. Computacional Science and Engineering. Vol. 3, No.2 (Summer, 1996). pp. 18-37.
http://cs.oregonstate.edu/~pancake/papers/IsParall/index.html
43
- Datos paralelos: HPF
Las tendencias de los códigos paralelos usando diferentes modelos de programación son:
Otros 6% 7%
Ninguno 4% 3%
44
Capítulo 4
MÉTRICAS DE RENDIMIENTO
4.1 Introducción
En la programación paralela, como en otras disciplinas de ingeniería, la meta del proceso de diseño
sirve para lograr un óptimo tiempo de ejecución, requerimientos de memoria, costos de
implementación, costos de mantenimiento, etc. Para ello, se tomaron en cuenta factores como
simplicidad, rendimiento, portabilidad, etc. Todo ello se logra con varios modelos matemáticos de
rendimiento los cuales nos dan cierta información con las métricas de rendimiento, como la eficiencia
de diferentes algoritmos, evaluar la escalabilidad, identificar cuellos de botella y otras ineficiencias, etc; y
con ellos podemos ver donde se necesita trabajar más para optimizar la aplicación y todo ello sin hacer
un costo sustancial de implementación.
Por rendimiento de una computadora se entiende la efectividad del desempeño de una computadora,
usualmente sobre una aplicación o un benchmark en particular. Esta noción de rendimiento incluye
velocidad, costo y eficiencia. Algunos de los factores que afectan el rendimiento de una computadora
son:
a) operaciones de registros.
b) operaciones de enteros.
d) operaciones en cadena.
- Tiempo de acceso a memoria para leer o escribir datos (recordando la jerarquía de la memoria).
a) en caché
b) en memoria principal
c) en memoria auxiliar
- Sistemas de archivos.
45
- Compiladores.
La discusión sobre la eficiencia de las métricas de rendimiento es bastante útil también, porque ellas
nos permite de una manera simple entender las caracterizaciones del poder de las supercomputadoras.
El rendimiento de las supercomputadoras es el atributo primario que hace a éstas superiores sobre
otras computadoras. Otra característica importante que se debe tomar en cuenta es la precisión
numérica, ésta es especialmente importante para aplicaciones de aritmética de punto flotante
implementados en cualquier máquina. Para la aritmética interna, el truncamiento por default, contrario
a la opinión popular, es usualmente cortar o redondeo hacia abajo, a menos que un tipo diferentes se
especifique y se requiera. Obviamente mientras más grande sea la palabra para la representación de los
números, los cálculos hechos por las computadoras serán más exactos, es decir, las aproximaciones de
los diversos eventos simulados se acercarán más al valor correcto. Es decir, el error tendrá cero.
También para una operación muy pequeña, siempre hay que tomar en cuenta el épsilon de la máquina
para que no se propaguen errores de redondeo.
Los números reales están representados mediante una representación empaquetada de mantisa y
exponente binarios. La palabra está representada según la IEEE en una representación de mantisa y
exponente y se ejemplifica en la siguiente figura. El exponente es una potencia de 2.
Debemos recordar que dependiendo del tipo de aplicación que se tiene y los requisitos de rendimiento
que se necesitan, el buen funcionamiento de nuestra aplicación dependerá de el tiempo de ejecución,
escalabilidad, mecanismo por el cual los datos son generados, guardados, transmitidos en las redes de
interconexión, obtenidos y guardados del disco, etc; además de que se deben considerar costos que
ocurren en las diferentes fases del ciclo de vida del software, incluyendo diseño, implementación,
ejecución, pruebas, mantenimiento, etc. Por lo tanto, las métricas para medir el rendimiento de nuestro
programa pueden ser tan diversas como: tiempo de ejecución, eficiencia de paralelismo, requerimientos
de memoria, latencia, throughput (cantidad de datos que se pueden transmitir por segundo), relación
inputs/outputs, costos de diseño, implementación, pruebas, potencial de reuso, requerimientos de
hardware, costos de hardware, portabilidad, escalabilidad, etc. Por lo regular, solo se necesita trabajar
en las métricas que más afectan el rendimiento y muy comúnmente las más importantes son el tiempo
de ejecución y la escalabilidad.
46
4.2 Algoritmia: Análisis asintótico:
Cuando tenemos que resolver un problema, es posible que estén disponibles varios algoritmos
adecuados. Evidentemente, desearíamos seleccionar el mejor. Esto plantea la pregunta de cómo
decidir entre varios algoritmos cuál es preferible. Si solamente tenemos que resolver uno o dos casos
pequeños de un problema más bien sencillo, quizá no nos importe demasiado qué algoritmo
utilizaremos: en este caso podríamos decidirnos a seleccionar sencillamente el que sea más fácil de
programar, o uno para el cual ya exista un programa, sin preocuparnos por sus propiedades teóricas.
Sin embargo, si tenemos que resolver muchos casos, o si el problema es difícil, quizá tengamos que
seleccionar de forma más cuidadosa.
El enfoque empírico (a posteriori) para seleccionar un algoritmo consiste en programar las técnicas
competidoras e ir probándolas en distintos casos con ayuda de una computadora. El enfoque teórico
(a priori) consiste en determinar matemáticamente la cantidad de recursos necesarios para cada uno de
los algoritmos como función del tamaño de los casos considerados. Los recursos que más nos
interesan son el tiempo de cómputo y el espacio de almacenamiento, siendo el primero normalmente el
más importante. En caso de un algoritmo paralelo, otro recurso muy importante es el número de
procesadores que se necesitan. La ventaja de la aproximación teórica es que no depende ni de la
computadora que se esté utilizando, ni del lenguaje de programación, ni siquiera de las habilidades del
programador. Se ahorra tanto el tiempo que se habría invertido innecesariamente para programar un
algoritmo ineficiente, como el tiempo de máquina que se habría desperdiciado comprobándolo. Lo
que es más significativo, se nos permite estudiar la eficiencia del algoritmo cuando se utilizan en casos
de todos los tamaños, es decir, determinar la actuación en una región mayor de lo que es un espacio
multidimensional grande y complejo. Esto no suele suceder con la aproximación empírica, en la cual
las consideraciones prácticas podrían obligarnos a comprobar los algoritmos sólo en un pequeño
número de ejemplares arbitrariamente seleccionados y de tamaño moderado.
Sin embargo, también resulta posible analizar los algoritmos utilizando un enfoque híbrido, en el cual la
forma de la función que describe la eficiencia del algoritmo se determina teóricamente, y entonces se
determinan empíricamente aquellos parámetros numéricos que sean específicos para un cierto
programa y para una cierta máquina, lo cual suele hacerse mediante algún tipo de regresión.
Empleando este enfoque se puede predecir el tiempo que necesitará una cierta implementación para
resolver un ejemplar mucho mayor que los que se hayan empleado en las pruebas. Sin embargo, hay
que tener cuidado cuando se hacen estas extrapolaciones basándose solamente en un pequeño número
de comprobaciones empíricas y anulando toda consideración teórica. Las predicciones hechas sin
apoyo teórico tienen grandes probabilidades de ser imprecisas, si es que no resultan completamente
incorrectas.
Para un algoritmo ordinario, secuencial, se suele considerar eficiente si su tiempo de ejecución para un
problema de tamaño n está en Ο(nk) para alguna constatne K. Por otra parte, para que se considere
eficiente un algoritmo paralelo, esperamos normalmente que satisfaga dos restricciones, una con
respecto al número de procesadores y la otra que concierne al tiempo de ejecución, las cuales son:
47
- el número de procesadores necesarios para resolver un caso de tamaño n debe estar en Ο(na)
para alguna constante a, y
- el tiempo requerido para resolver un caso de tamaño n debe estar en Ο(logb n) para alguna
constante b.
Un algoritmo paralelo se dice óptimo si es más eficiente con respecto el mejor algoritmo secuencial
posible. A veces se puede denominar óptimo si es más eficiente con respecto al mejor algoritmo
secuencial conocido. En este caso, sin embargo, es preferible decir que el problema correspondiente
tiene aceleración óptima. Recordemos, también, que hay muchos problemas para los cuales no se
conoce ningún algoritmo secuencial eficiente (esto es, de tiempo polinómico). Para tales problemas, no
podemos esperar hallar una solución paralela eficiente (esto es, una que utilice un número polinómico
de procesadores y un tiempo polilogarítmico). Por otra parte, hay muchos problemas para los cuales se
conoce un algoritmo secuencial eficiente, pero para los cuales todavía no se ha descubierto un
algoritmo paralelo eficiente. Se cree, aunque no se ha demostrado, que algunos problemas que se
pueden resolver mediante un algoritmo secuencial eficiente no poseen una solución paralela eficiente.
Encontrar el algoritmo óptimo es muy importante para lograr resolver problemas de gran reto,
independientemente del avance del hardware, ya que sólo así se podrán resolver esots problemas con
un tamaño considerable de datos y en tiempos aceptables. También hay que tener en cuenta que el
análisis asintótico funciona muy bien para N y P grandes y no para tamaños de problema y
procesadores reales que se tienen, además no toma en cuenta los costos de comunicación y otros ya
que toma consideraciones de máquinas idealizadas tipo PRAM (donde el costo de comunicación es
nulo) que son un tanto diferentes a las máquinas reales.
Dos ejemplos de que tenemos que tomar varias consideraciones son que pro ejemplo considérese dos
algoritmos cuyas implementaciones en una cierta máquina requieren n2 días y n3 segundos
respectivamente para resolver un caso del tamaño n. Solamente en casos que requieran más de 20
millones de años para resolverlos, el algoritmo cuadrático será más rápido que el algoritmo cúbico.
Desde un punto de vista teórico, el primer es asintóticamente mejor que el segundo; esto es, su
rendimiento es mejor para todos los casos suficientemente grandes. Sin embargo, desde un punto de
vista práctico, preferimos ciertamente el algoritmo cúbico. Otro ejemplo es para tres algoritmos, donde
el tiempo de ejecución se da con las siguientes ecuaciones:
N2
T1 = N +
P
N + N2
T2 = + 100
P
N + N2
T3 = + .6 P 2
P
48
Estos algoritmos tienen un speedup de aproximadamente 10.8 cuando P=12 y N=100. Sin embargo,
se comportan completamente diferentes en otras situaciones como se muestra en la siguiente gráfica.
Todos los algoritmos no tienen un muy buen rendimiento a P grandes y el algoritmo con T3 claramente
es el peor de todos. A N=100 el algoritmo con T1 y T2 se comportan casi igual; sin embargo si
N=1000, el algoritmo con T2 es muy bueno, casi logra un speedup perfecto.
49
4.3 Rendimiento de computadoras paralelas (Un primer acercamiento)
La ganancia (incremento) de velocidad que puede conseguir una computadora paralela con n
procesadores idénticos trabajando concurrentemente en un solo problema es como máximo n veces
superior a la de un procesador único. En la práctica la ganancia es mucho menor, ya que algunos
procesadores permanecen inactivos en algunos instantes debido a conflictos en los accesos a memoria,
las comunicaciones, uso de algoritmos ineficaces en la explotación de la concurrencia natural del
problema, etc. En la figura se muestran las diferentes estimaciones de la ganancia real de velocidad,
que ven desde una cota inferior de log2 n hasta una cota superior de n/Ln n.
1000
n (caso ideal)
100
Ganancia de velocidad
n/Ln n
10
Conjetura de Minsky
1
2 4 8 16 32 64 128 256 512 1024
Número de procesadores
n
1
n ∑i
Tn= ∑ f i ·d i = i =1
i =1 n
Donde la suma representa los n modos de operación. La ganancia media de velocidad G se obtiene
como razón entre T1=1 y Tn; es decir,
T1 n n
G= = n ≤
1 Ln(n)
∑
Tn
i =1 i
Para un sistema con 2, 4, 8, o 16 procesadores, las ganancias medias respectivas son 1.33, 1.92, 3.08 y
6.93. El incremento de velocidad (G) puede ser aproximado por n/Ln n para valores grandes de n.
Por ejemplo, G=1000/Ln 1000=144.72 para n=1000 procesadores.
Las consideraciones que hasta ahora hemos hecho, son muy ideales; ya que se supone que todos los
procesadores trabajan todo el tiempo y al 100%, el tiempo de comunicación es nulo, etc. Por ello, es
necesario tener modelos que sean más realistas pero que al mismo tiempo no se pierda la simplicidad
del modelo. Un modelo muy utilizado y que cumple con estas características es la ley de Amdahl.
Aquí, tomamos en cuenta que durante el proceso paralelo, siempre existe un componente secuencial
que limitará el speedup que puede lograrse en una computadora paralela. El Speedup, es la proporción
entre tiempo de ejecución de un solo procesador y el tiempo de ejecución en los n procesadores de la
máquina paralela. La ley de Amdahl nos dice que si el componente secuencial de un algoritmo es 1/s
del tiempo de ejecución total del programa, entonces el posible speedup máximo que puede lograrse es
s. Por ejemplo, si el componente secuencial es 5%, entonces el speedup máximo que puede lograrse es
20. Matemáticamente, esto es:
α
Tp=[1-α+ ]*T1 .
p
Donde la ejecución o tiempo de CPU en los p procesadores paralelos depende de la fracción paralela α
que es proporcional al tiempo de un procesador T1 (que por lo regular es utilizando los mejores
algoritmos seriales). El Speedup, o el factor de aceleración, se define matemáticamente así:
51
T1 1 1
Sp = = = donde s=1-α. Es decir, s es el componente secuencial del algoritmo.
Tp α α
1−α + s+
p p
Otro caso interesante es cuando el porcentaje paralelo es 100%, entonces el Sp tendrá un speedup
máximo de infinito y regresamos al caso ideal que mostramos en la gráfica 4.1. La ley de Amdahl se
comporta parecidamente a la conjetura de Minsky; pero a diferencia de éste, la asíntota puede variar
dependiendo del porcentaje del componente paralelo del algoritmo.
Los aspectos importantes de la ley de Amdahl es que no toma en cuenta la sincronización de los
procesos y la sobrecarga originada por dicha sincronización, es decir, omite los tiempos de las
intercomunicaciones existentes entre procesadores en el procesamiento paralelo. Descartando u
omitiendo estas intercomunicaciones es imposible obtener un buen rendimiento, sino se tiene en
cuenta esto y que en ocasiones las comunicaciones son más tardadas que el cálculo mismo, no se
obtendrá un buen rendimiento de los procesos. Entonces es importante conocer una manera óptima o
fiable de llevar a cabo la repartición de los datos (el esquema de paralelización) para tener el menor
número de intercomunicaciones posibles. Por lo tanto, un factor importante es la minimización de la
razón comunicación/cómputo.
52
Número de CPUs Speedup Teórico
1 1.000
2 1.937
3 2.818
4 3.647
5 4.428
6 5.167
7 5.863
8 6.525
9 7.152
10 7.752
… …
∞ 30.959
Notemos que las curvas cambian conforme cambia el tamaño del problema. Si se aumenta el tamaño
del problema de tal manera que el porcentaje paralelizable aumenta (E/S se utilizan en menos tiempo
que del total, etc), entonces el speedup mejorará; pero sucederá exactamente lo contrario si el
porcentaje paralelizable disminuye (la sincronización mata el paralelismo). Es importante considerar la
variación del speedup con respecto al tamaño del problema tal y como se ve en el análisis asintótico.
53
4.5 Aceleración superlineal
De acuerdo a lo que hemos visto, no es posible obtener una aceleración superior a la lineal. Esto esta
basado en el supuesto de que un único procesador siempre puede emular procesadores paralelos.
Suponga que un algoritmo paralelo A resuelve una instancia del problema B en Tp unidades de tiempo
en una máquina paralela con p procesadores. Entonces el algoritmo A puede resolver la misma
instancia del problema en pTp unidades de tiempo en la misma máquina pero usando un solo
p
procesador. A esto se le llama potencia (aunque la potencia real se da por ∑T
i =1
i , dependiendo del
tiempo de cada procesador, lo que complicaría mas los cálculos). Por lo tanto, la aceleración no puede
ser superior a p. Dado que usualmente los algoritmos paralelos tienen asociados costos adicionales de
sincronización y comunicación, es muy probable que exista un algoritmo secuencial que resuelva el
problema en menos de pTp unidades de tiempo, haciendo que la aceleración sea menor a la lineal. Sin
embargo, hay circunstancias algorítmicas especiales que provocan que la aceleración puede ser mayor a
la lineal. Por ejemplo, cuando se esta resolviendo un problema de búsqueda, un algoritmo puede
perder una cantidad de tiempo considerable examinando estrategias que no llevan a la solución. Un
algoritmo paralelo puede revisar muchas estrategias simultáneamente y se puede dar el caso de que una
de ellas de con la solución rápidamente. En este caso el tiempo del algoritmo secuencial comparado
con el paralelo es mayor que el número de procesadores empleados.
También existen circunstancias de arquitectura que pueden producir aceleraciones superlineales. Por
ejemplo, considere que cada CPU en una máquina paralela tiene cierta cantidad de memoria caché.
Comparando con la ejecución en un solo procesador, un grupo de p procesadores ejecutando un
algoritmo paralelo tiene p veces la cantidad de memoria caché. Es fácil construir ejemplos en los que la
tasa colectiva de hits de caché para los p procesadores sea significativamente mayor que los hits de
caché del mejor algoritmo secuencial corriendo en un solo procesador, reduciendo los costos de
54
accesos a memoria. En estas condiciones el algoritmo paralelo puede correr más de p veces más
rápido. Estas circunstancias son especiales y se dan raras veces.
En los modelos anteriores se tiene la ventaja de que son sencillos para hacer predicciones rápidas; sin
embargo, no toman en cuenta muchos aspectos que suelen ser importantes para los tiempos de
ejecución reales. El tiempo de ejecución es una función multidimensional que depende del tamaño del
problema, número de procesadores, número de procesos y otras características del algoritmo y del
hardware, es decir:
T = f ( N , P, U ,...)
Nosotros definimos el tiempo de ejecución de un programa paralelo como el tiempo que transcurre
desde que el primer comienza la ejecución del problema hasta que el último procesador completa la
ejecución. Durante la ejecución, cada procesador se encuentra haciendo cómputo, comunicando o está
de ocioso como se ilustra en la siguiente figura.
El tiempo de ejecución total lo definiremos como la suma de los tiempos de comunicación, cómputo y
tiempo de ocio de cada procesador divididos por el número total de los procesadores, es decir:
55
1 p −1 i p −1 p −1
T= ∑ Tcomputación + ∑ Tcomunicaci
i
ón + ∑ i
Tocio
P i =0 i =0 i =0
Ésta ecuación se puede hacer más complicada si se toman en cuenta las jerarquías de memoria, la
topología de red de la interconexión, costos de inicialización de comunicación o cómputo, etc.
También se pueden tomar en cuenta estudios empíricos para calibrar ésta ecuación en lugar de usar un
modelo más complejo de primeros principios.
El tiempo de cómputo depende del tamaño del problema, número de procesos o procesadores, tipos
de procesadores, jerarquía de la memoria, etc. Por lo tanto, es muy común obtener éste tiempo en base
a estudios empíricos y/o mediciones indirectas. El tiempo de comunicación es el tiempo que los
procesos utilizan para enviar y recibir los mensajes que se hacen durante la ejecución. La comunicación
es puede ser interprocesador (entre diferentes procesadores) e intraprocesador (en el mismo
procesador). Como vimos en el capítulo dos, en una máquina paralela idealizada, el costo de enviar un
mensaje entre dos procesos localizados en diferentes procesadores puede ser representada por dos
parámetros: el tiempo inicio del mensaje ts que el tiempo necesario para comenzar la comunicación y el
tiempo de traslado por palabra tw, el cual está determinado por el ancho de banda de la red de
interconexión. Entonces, el tiempo que se necesita para enviar un mensaje de L palabras de tamaño es:
Tmsg=ts+twL
Estos datos por lo regular se obtienen mediante un programa que mide el tiempo que se tarda en
mandar un mensaje de un procesador a otro. Entre más grandes sean los mensajes, entonces ésta
ecuación funcionará mejor y asintóticamente para L muy grandes, solamente el término tw será el
importante. En cambio, cuando los mensajes son muy pequeños, entonces el término más importante
será el ts.
Cuando una aplicación genera muchos mensajes, como regularmente ocurre, entonces muchas veces es
necesario refinar éste modelo y esto se logra desarrollando un modelo más detallado y que tome en
cuenta la red de interconexión. Aquí es muy importante tomar en cuenta que mientras un procesador
está enviando datos a través de la interconexión, otros procesador no puede hacer lo mismo utilizando
la misma ruta; por ello existen el algoritmo de envío y el mecanismo de control de flujo como se vio en
el capítulo 2. Y como en dicho capítulo vimos, el tiempo de comunicación o latencia se puede expresar
como: Tiempo(n)=Sobrecarga + retardo de envío + ocupación del canal + retraso de la contención.
El Speedup decrece aproximadamente a:
56
T
Sp = <p
T
+ Tcomunicación
p
y para que la aceleración no sea afectada por el tiempo de comunicación necesitamos que:
T T
>> Tcomunicación ⇒ p <<
p Tcomunicación
Esto significa que a medida que se divide el problema en partes más y más pequeñas para poder
ejecutar el problema en más procesadores, llega un momento en que el costo de comunicación se hace
muy significativo y desacelera el cómputo.
En cuanto al tiempo de ocio, por lo regular, es el más difícil de determinar y se hace comúnmente con
mediciones indirectas. Un procesador puede estar ocioso a falta de cómputo o de datos. En el primer
caso, se puede evitar con un balanceo de carga eficiente y en el segundo caso si el programa es lo
suficientemente robusto para que los procesadores realicen otro cómputo o comunicación mientras
esperan por datos remotos. Esta técnica se llama traslapando cómputo y comunicación (overlapping
computation and communication). Esto se puede lograr ya sea que se generen múltiples tareas o
procesos en un mismo procesador pero solo es eficaz si el costo de programar una nueva tarea es
menor al costo del tiempo de ocio; y la otra manera se logra explícitamente programando otros cálculos
mientras se espera la comunicación como se muestra en la siguiente figura. Muchas bibliotecas
permiten éste tipo de comunicación, por ejemplo en MPI la comunicación puede ser bloqueante o no
bloqueante para permitir el solapamiento de la comunicación y cómputo.
57
4.7 Eficiencia
Como vemos aquí y en el análisis asintótico, un programa o algoritmo serial optimizado, robusto y bien
diseñado es muy importante, ya sea para un posterior paralelización o para obtener buenas métricas
con respecto al programa paralelo obtenido.
i =0 i=0
i
i =0
i
entonces para mantener la eficiencia constante se tiene que cumplir que el tiempo que transcurre en un
solo procesador, debe aumentar a la misma proporción al aumento del tiempo total de cómputo,
comunicación y ocio que ocurren en el cómputo paralelo.
Con los análisis que hemos hecho hasta ahora, podemos tener un perfil más o menos amplio sobre el
comportamiento y rendimiento del algoritmo ya sea en diferentes máquinas, tamaños de problema, etc.
Sin embargo, todavía no se tiene una base suficiente para responder las siguientes preguntas:
- ¿el algoritmo cumple con los requerimientos (tiempo de ejecución, requerimientos de memoria,
etc.) en la máquina paralela designada?
- ¿qué tan adaptable es mi algoritmo? Es decir, ¿qué tanto se afecta con los aumentos del
tamaño del problema o con los parámetros dependientes de la máquina como ts y tw?
58
- ¿Qué diferencia en tiempo de ejecución puede esperarse con los diferentes algoritmos?
Para poder responder bien éstas preguntas, se necesitan de estudios empíricos ya que una vez que
nuestro algoritmo es implementado, podemos validar nuestros modelos e inclusive mejorarlos con
respecto a los recursos que tenemos. Recordemos, que la programación paralela, todavía es por encima
de todo una disciplina experimental.
El primer paso en un estudio experimental es la identificación de los datos que deseamos obtener,
como por ejemplo ts tw, etc, y éstos se varían con respecto a otras variables, como tamaño de problema,
numero de procesadores, etc, obteniendo así un rango de datos, el cual nos dirá en que región mejor se
adaptará nuestro modelo, un método común, es el de mínimo cuadrados. Maximizando el número de
datos que se obtienen, reducimos el impacto de errores de medición. El segundo paso, es el diseño de
los experimentos el cual nos darán los datos que necesitamos obtener. El problema crítico aquí es
asegurar que nuestros experimentos realmente midan lo que nosotros necesitamos medir, además de
que sean datos reproducibles y lo más precisos posibles. Siempre deben repetirse los experimentos
para verificar que los resultados sean reproducibles y generalmente, los resultados no deben variar mas
del 2% del valor. Recordemos que las posibles causas de variación son:
- costos de inicio y terminación, los cuales suceden debido al estado del sistema que puede ser
muy variable. Para obtener una buena medición es preferible comenzar el cronómetro después
de que los componentes del sistema han sido terminados y detenerlo una vez que el resultado
ha sido computado.
- interferencia de otros programas, debido a la competencia que existe por otros usuarios,
programas, etc. Para evitar esto, es mejor hacer las mediciones en un tiempo donde la
competencia (carga de trabajo) del sistema sea menor y/o preferiblemente usar sistemas de
colas que nos permitan tener ciertos recursos del sistema asignados y dedicados a nuestra
aplicación en el tiempo que se ejecute éste, además de tener un buen calendarizador.
Los estudios de variabilidad en los resultados experimentales pueden ayudarnos a identificar fuentes de
error o incertidumbres en nuestras medidas. Sin embargo, incluso cuando los resultados son
reproducibles, todavía no tendremos la certeza de que ellos sean correctos; por ello, es importante
59
medir la misma métrica mediante diferentes maneras y verificando que los resultados de estas
redundantes mediciones sean consistentes.
4.9 Entradas/Salidas
- checkpoints: muchos cómputos realizados en máquinas paralelas que se ejecutan por periodos
de tiempo extendidos, tienen periódicos puntos de control (checkpoints) que nos ayudan en la
tolerancia de fallas.
- memoria virtual: muchos programas utilizan estructuras de datos que ocupan un espacio mayor
de memoria a la disponible físicamente en el procesador (memoria caché y RAM) y es por ello
que se utiliza la memoria virtual para realizar la paginación necesaria de datos al disco,
provocando que el rendimiento del programa caiga mucho por ello, es por eso, que la
migración explícita de datos suele utilizarse para mejorar el rendimiento del programa.
- análisis de datos: muchas aplicaciones involucran un análisis de grandes cantidades de datos las
cuales son particularmente demandantes desde un punto de vista de I/O (más que de cómputo
numérico) ya que se hace un cómputo relativamente pequeño entre cada dato que se recupera
de disco; y por lo tanto, el análisis de los datos es más importante que el análisis del cómputo.
Un ejemplo clásico son las bases de datos de las transacciones bancarias que ocurren.
Es difícil proporcionar una discusión general de I/O paralelo debido a que las diferentes máquinas
paralelas tienen radicalmente diferentes arquitecturas y mecanismos de I/O. Sin embargo, algunos
puntos que se deben tratar son:
- podemos pensar en los I/O como comunicaciones entre los procesadores de tal manera que
podemos determinar un costo de inicio y de transferencia de palabra por tiempo para medir el
costo de los I/O en nuestra aplicación. Obviamente, el costo de inicio en I/O es mucho
mayor al de las comunicaciones interprocesador. Entonces, se pueden utilizar las mismas
técnicas para minimizar los costos como minimizar los inicios, etc.
- en la mayoría de las computadoras paralelas, tienen diferentes caminos para que los
procesadores puedan hacer I/O concurrentemente, por lo tanto nosotros buscaremos
60
organizar los I/O para que diferentes procesadores lean y escriban concurrentemente
(obviamente manteniendo la coherencia) usando diferentes estrategias, rutas o caminos.
- es importante conocer el modo de trabajo del sistema de archivos de la máquina para conocer
sus ventajas y aprovecharlas además de evitar sus desventajas y obtener así un mejor
rendimiento.
Hasta aquí, hemos visto como hacer un análisis y diseño de programas paralelos, así como diferentes
modelos que nos permiten ver la buena o mala actuación de nuestra aplicación conforme a varias
métricas como el tiempo de ejecución, eficiencia, escalabilidad, etc. con respecto a diferentes variables
como tamaño del problema, número de procesadores, parámetros de comunicación, etc. Todas estas
técnicas pueden utilizarse durante todo el ciclo de la aplicación, el cual es:
- primero se hace el análisis del problema para determinar que tan eficiente puede ser un
algoritmo paralelo para nuestras necesidades creando así un diseño que se caracteriza por los
requerimientos de cómputo y comunicaciones que se tienen.
- Después se analizan las diferentes alternativas que tenemos para identificar las áreas de
problema como los cuellos de botella y para verificar que los algoritmos reúnen los requisitos
de rendimiento y actuación.
- por último durante la implementación, nosotros compararemos la actuación real del programa
paralelo con su modelo de actuación ideal. Esto nos puede ayudar a identificar los errores de la
implementación y mejorar así la calidad del modelo.
El último paso, se logra al obtener un perfil del programa obtenido durante la ejecución. Este perfil es
muy importante ya que solo así podremos atacar los cuellos de botella de nuestra aplicación y obtener
así un programa optimizado, lo que se verá en el siguiente capítulo. Por último, debemos recordar que
todos los pasos durante el ciclo de la aplicación, afectan o pueden afectar pasos anteriores, generando
así un proceso de software tipo espiral.
61
Bibliografía:
- Foster, I. Designing and Buildind Parallel Program: Concepts and Tools for Parallel Software
Engineering. Addison-Wesley, New York, 1995. http://www.mcs.anl.gov/dbpp
62
Capítulo 5
5.1 Introducción
Como hemos visto a lo largo de los capítulos, el proceso de cómputo constantemente incrementa su
complejidad, demandando mayor calidad efectiva y gran desempeño en su ejecución, controlando y
reduciendo el tiempo de proceso, adquiriendo equipo más rápido, agregando memoria y usando
conexiones de redes más rápidas. Por lo tanto, se deben diseñar programas que hagan mejor uso de
estos costosos y limitados recursos, usar nuevas técnicas de programación y tecnologías de innovación
para mejorar la velocidad de ejecución; por eso, es muy importante que la programación se de mediante
una metodología que produzca software de calidad aceptable y que la utilización de las opciones de
compilación de las herramientas, por ejemplo, sean las indicadas para una utilización eficiente. La
optimización de código, como hemos visto, puede realizarse durante la propia generación de éste o
como paso adicional, ya sea intercalando entre el análisis semántico y la generación de código o situado
después de ésta (se optimiza a posteriori el código generado). Hay teoremas (Aho, 1970) que
demuestran que la optimización perfecta es indecidible, en consecuencia, las optimizaciones de código
en realidad proporcionan mejoras, pero no aseguran el éxito total.
La optimización es un factor básico para obtener un alto desempeño en equipos de cómputo, por ello,
el desarrollo de software procura alcanzar este objetivo, por lo tanto, los resultados deben ser los
esperados, que la escritura del código sea adecuada y se realice en el tiempo requerido, resultando que
cada vez sea más productivo realizarlo. Para obtener un buen rendimiento de nuestro programa
paralelo se requieren de varios factores los cuales ya sean planteado a lo largo de los capítulos anteriores
y donde se aterrizarán en el ejemplo que utilizaremos de cómputo científico: DFT++. Los principales
factores para un buen paralelismo y comportamiento de nuestros programas paralelos son:
63
La optimización de una aplicación consta de una serie de etapas a realizar, iniciando por el código
fuente, mejorando las líneas del código, de modo que resulte un código más rápido de ejecutar. El
tiempo de ejecución de un programa siempre dependerá en gran medida de la arquitectura de la
computadora en la que se esté ejecutando, por ello no es lo mismo hablar de ejecución en máquinas
cuyo procesador es RISC a que sea CISC, que tenga niveles de caché intermedio a que no los tenga, etc.
Es por ello, que durante éste capítulo, la optimización de los códigos se enfocará solamente en la
máquina Origin 2000; sin embargo, y debido a que durante los capítulos anteriores se hizo una visión
general de las máquinas paralelas, al lector no le será difícil reconocer las diferencias y similitudes entre
las diferentes máquinas y lograr así una visión general de la optimización de códigos en el cómputo de
alto rendimiento. También recordemos que el vendedor de las máquinas paralelas, por lo regular,
ofrecerá documentos que nos permitirán conocer las bondades de dicha máquina y como utilizarlas de
la mejor manera para lograr optimizar las códigos que utilizaremos; con excepción de los clusters
hechos en “casa”, donde el “humanware” disponible toma un papel sumamente importante. Sin
embargo, siempre es recomendable hacer diferentes benchmarcks en diferentes máquinas para conocer
sus comportamientos en los códigos que más utilizaremos y tomar así una mejor visión de nuestras
necesidades de hardware y software ya que los datos que nos dan los vendedores siempre son los picos
del comportamiento de la máquina, es decir, en condiciones ideales para su mejor funcionamiento; por
lo tanto, para la mayoría de nuestras aplicaciones paralelas, la actuación será entre el 10 y el 20% del
comportamiento máximo de la máquina.
Recordemos que con la demostración de Sahni, la única métrica totalmente fiable, es el tiempo de
cómputo total para nuestra aplicación paralela en particular y dicha métrica no puede conocerse
exactamente por adelantado ni con el uso de estadísticas de otra aplicación; no importando lo similar
que es en estructura y propósito. Es por ello de la gran importancia de los benchmarcks que se deben
hacer a diferentes arquitecturas y condiciones de ejecución para así tener un mejor conocimiento del
comportamiento de nuestra aplicación y de las diferentes arquitecturas de las máquinas paralelas.
Los números de punto flotante definidos con REAL, DOUBLE PRECISION y COMPLEX para
fortran y flota, double y long double para C, son representaciones inexactas de los números reales, al
igual que las operaciones realizadas con ellos. Los compiladores de IRIX generan código de punto
flotante de acuerdo a los estándares de la IEEE 754 (como se explicó en el capítulo 3). Si se desea
rapidez y no es tan importante la exactitud de los resultados se puede implementar el nivel 3 de
optimización, donde se realizan transformaciones de expresiones aritméticas en el código sin considerar
los estándares. Además los compiladores MIPSpro proporcionana opciones fuera del alcance de IEEE
754. Estas opciones permiten transformaciones de cálculos específicos del código fuente los cuales
podrían no producir los mismos resultados del punto flotante, aunque ellos involucran un cálculo
matemáticamente equivalente. Las principales opciones controlan la exactitud de las operaciones de
punto flotante y los comportamientos de excepción de “overflow” y “underflow”. El control de la
exactitud de punto flotante es realizado con la opción –OPT:roundoff=n
64
La opción “roundoff” especifica cuales optimizaciones son admitidas para afectar o no los resultados
de punto flotante, en términos de exactitud y comportamiento overflow/underflow. Acepta los
siguientes valores para n=0,1,2,3.
La opción roundoff=0 no realiza transformaciones que puedan afectar los resultados de punto flotante.
Este es el nivel de default para la optimización. La opción roundoff=1 admite transformaciones con
efectos limitados en los resultados de punto flotante. Los límites significan que sólo el último o los dos
últimos bits de la mantisa serán afectados. Para overflow(underflow) significa que los resultados
intermedios de los cálculos pueden originar un “overflow” con un factor de dos de lo que la expresión
original pudo haber originado en overflow(underflow). Note que los límites realizados pueden ser
menos limitados cuando son constituidos de múltiples transformaciones. Por ejemplo, esta opción
admite usar la instrucción rsqrt (raíz cuadrada), la cual es ligeramente menos exacta (por
representaciones internas de los tipos de datos) pero significativamente más rápida.
La opción roundoff=2 admite transformaciones con efectos más extensos en el resultado de punto
flotante. Permite rearreglos asociativos, iteraciones en ciclos y distribución de la multiplicación sobre
adición/substracción. Y la opción roundoff=3 admite cualquier transformación matemática válida de
expresión de punto flotante. Permitiendo inducción de variables de punto flotante en lazos, y
algoritmos rápidos, valores absolutos y divisiones complejas.
Los compiladores de IRIX ofrecen una amplia variedad de niveles de optimización al realizar la
compilación. El uso inadecuado de las opciones de los niveles de optimización puede generar
resultados incorrectos, por tal razón, es necesario que el programador conozca y entienda los tipos de
optimización que el compilador puede realizar. Es importante mencionar que los compiladores de C o
Fortran toman por omisión algunas opciones cuando éstas no se especifican en la orden de
compilación. La Origin 2000 de la UNAM tiene por default: -n32 –mips4 –r10000 –O0, donde:
-n 32 Usa el nuevo ABI (Application Binary Interface) de 32 bits. El de 64 bits (-n64) se usa
sólo cuando el programa requiere más de 2 GB de direccionamiento en memoria y se
busca una mayor precisión de algunos cálculos, se debe tener en cuenta que habrá una
pérdida de velocidad y se requerirá de más memoria.
-mips4 Compila el código optimizado para el CPU R10000, R5000 Y R8000 usando el conjunto
de instrucciones MIPS IV.
-R10000 Código compilado y optimizado para el procesador R10000
-O0 Nivel de optimización 0. Sin optimizar.
Las opciones configuradas por omisión se pueden observar con las siguientes instrucciones:
%cc prueba.c –show_defaults
%cc prueba.c –LIST:=ON (genera el archive prueba.l); es lo mismo para f77 y f90.
65
La siguiente tabla describe brevemente cada uno de los niveles de optimización de los compiladores de
la Origin 2000.
Opción Descripción
Nivel Optimizaciones
66
Las opciones para las optimizaciones de ciclos anidados son:
67
Desenrollamiento #pragma C*$* UNROLL(n) Especifica que se desea un
de ciclos unroll(n) desenvolvimiento en el ciclo de
n veces.
Bloqueo de caché #pragma no C*$* NO BLOCKING Previene el bloqueo de un
blocking móduclo o de un ciclo anidado.
#pragma C*$* BLOCKING SIZE Especifica el tamaño de bloque
blocking size ([l1,l2]) para caché L1, caché L2 o
([l1,l2]) ambos.
Preidentificación #pragma C*$* PREFETCH() Activa o desactiva la
prefetch preidentificación para el caché
de nivel n.
#pragma C*$* Valida o invalida la
prefetch_manual PREFETCH_MANUAL() preidentificación manual.
Las opciones recomendadas para iniciar la optimización son:
-n32 Usa el 32-bit ABI, use -64 sólo cuando el programa necesite más de
2GB de espacio virtual de direcciones.
68
5.4 Sugerencias en la optimización en la Origin 2000
Las sugerencias son aplicar al código la mayor cantidad de técnicas de optimización manual posibles, es
recomendable que en la programación matemática se considere el uso de bibliotecas científicas
optimizadas para obtener un mejor desempeño del programa, verificar que los resultados sean los
correctos al momento de ejecutar el programa, sobre todo cuando se usa un nivel más alto de
optimización en la compilación del programa, obtener un perfil o perspectiva de la ejecución del
programa, esto es, detectar en qué partes del código se consume mayor tiempo de CPU, memoria e
incluso uso de dispositivos periféricos. La finalidad es usar esta información para minimizar el uso de
estos recursos. Cuando el programa es extenso en módulos y líneas de código, el perfilado será de
mucho beneficio, dado que deberá mostrar en qué parte del código se está realizando la mayor carga de
trabajo, y sobre ésta recomendar la aplicación de optimizaciones automáticas necesarias. En la Origin
2000 se pueden emplear herramientas como perfex (reporta el conteo de los contadores del procesador
R10000), speedshop con ssrun (colecciona datos que permiten identificar las regiones de mayor
consumo de recursos), prof, time y timex para medir el tiempo de ejecución del programa,
instrucciones, etc. Una vez detectado en que partes del código se realiza la mayor carga de trabajo y en
que partes disminuye el desempeño del mismo, aplicar el nivel adecuado de optimización que provee el
compilador. En este punto hay dos recomendaciones importantes: si es muy importante la exactitud de
los resultados en punto flotante es recomendable aplicar la optimización del nivel 2 y si el programa
contiene ciclos anidados, es recomendable emplear el nivel 3 de optimización.
Al usar el nivel 3, se deberá observar que los resultados no varíen demasiado con respecto a los
obtenidos en niveles inferiores o con los del programa ejecutado sin uso de la optimización automática.
Es criterio del programados el empleo de estos paso debido a la diversidad de programas que existen, el
origen del programa, la experiencia del programador y sobre todo el grado de interés para optimizar su
programa.
Los pasos indicados para optimizar son los más recomendables para lograr que el programa sea más
eficiente. El usuario debe conocer el comportamiento de su programa, obtener un perfil o perspectiva
del mismo y aplicar los niveles adecuados de optimización que presentan los compiladores. La
descripción de optimizaciones enfocadas a la arquitectura de la Origin 2000 permite a los usuarios
conocer y aprovechar estas características, que beneficiarán la ejecución de los programas sobre ésta
plataforma. La optimización es el primer paso para afinar un programa, el segundo paso será ejecutar
el programa optimizado sobre múltiples CPUs, logrando todavía un incremento en la eficiencia del
programa y después optimizar dicho programa para los múltiples CPUs.
69
70
Capítulo 6
UN EJEMPLO: DFT++
6.1 Introducción
Una vez que hemos visto como optimizar un código en una máquina paralela, es importante aplicar
todos éstos conocimientos y para ello, tendremos un ejemplo; el cual es un código de química
computacional llamado DFT++ desarrollado en el MIT por el grupo del profesor Tomás Arias
(http://dft.physics.cornell.edu/). Este programa es muy interesante, ya que fue desarrollado en C++
utilizando el paradigma de programación Orientado a Objetos, el cual, es muy raro en el cómputo
científico; y esto se debe a que la gran mayoría de los científicos, programan en Fortran que es un
lenguaje del cual se puede obtener fácilmente un alto rendimiento.
Sin embargo, los estudiantes de las ciencias de la computación, programan principalmente en C, C++ y
Java utilizando ya sea la programación estructura o la orientada a objetos, ésta última ha tenido una
altísima aceptación debido a las grandes posibilidades que puede dar dicho paradigma de programación
a la ingeniería de software (robustez, bajo costo de mantenimiento, código fácil de entender, etc). Este
es un pequeño problema que ha ocurrido durante los primeros años del cómputo científico, donde en
sólo unos pocos años se han introducido conceptos importantes como los tipos de datos abstractos
(TDA) y sus aplicaciones, el uso de metodologías para la programación y la ingeniería de software, etc.
Es por ello, que se piensa que existirán en un futuro una mayor cantidad de programas científicos
escritos mediante la programación orientada a objetos utilizando C++ existiendo así una mejor
comunicación entre los científicos e ingenieros y los programadores. Pero es muy importante señalar,
que el análisis y diseño de la aplicación es muy importante y que en C++ el alto rendimiento se alcanza
solo si existió un buen análisis y diseño de la aplicación.
Recordemos que la eficiencia es un punto importante en el cómputo científico y en C++, es muy fácil
perderla, por ello es muy importante conocer las fuentes de bajo rendimiento y evitarlas (como por
71
ejemplo utilizar templates, utilizar compiladores eficientes, etc.). Entre los puntos importantes que
debemos recordar son:
- el uso de templates, como en la STL (Estándar Template Library) provee mucha generalidad,
flexibilidad y eficiencia.
- Evitar mediante un buen diseño del programa el costo adicional por creación y destrucción de
cada objeto.
- evitar lo mayor posible el aliasing que impiden la optimización automática del compilador.
72
Recordemos que el compilador puede insertar código ineficiente durante la creación del ejecutable sin
avisar, por lo tanto, escribir código eficiente en C++ requiere de conocer y desarrollar nuevas técnicas
específicas del lenguaje. También se tiene que recordar, que los constructores y destructores de un
objeto, se ejecutan automáticamente justo en el momento de la declaración y cuando el flujo del
programa sale de su ámbito, por ello es importante evitar la construcción y destrucción de objetos que
no se usan, mediante apuntadores y solo construyendo con new cuando se usan los objetos; además de
evitar poner operaciones no necesarias dentro de los constructores y destructores. Con la herencia, el
problema se puede agrandar, ya que la construcción de objetos ocasiona la ejecución recursiva de los
constructores de las clases padre y de las clases miembro; por ello, es importante hacer las jerarquías de
clases lo más simple posible, asegurarse de que el código utilice todos los objetos que se construyan y
no utilizar herencia ni composición a menos que sea muy necesario y preferentemente utilizar otras
técnicas como templates.
Otro problema grave de bajo rendimiento son la generación de objetos temporales, los cuales muchas
veces son generados de manera silenciosa por el compilador y generalmente aparecen en la evaluación
de operaciones binarias definidas entre objetos. Por ejemplo:
73
En éste caso, lo que ocurre es una conversión de tipos; con la palabra reservada explicit, se evita la
conversión o también se puede definir el operador de asignación para evitar la creación de temporales.
Para arreglos, que se utilizan muy frecuentemente en el cómputo de alto rendimiento, existe una gran
cantidad de factores que pueden causar bajo rendimiento. Para arreglos pequeños, la sobrecarga de
new y delete causan un rendimiento muy malo (1/10 de su contraparte en C/F77). Para arreglos de
tamaño medio (in-cache), además del tiempo extra gastado en los ciclos adicionales, es posible que nos
salgamos del cache (out-of-cache). Para arreglos grandes (out-of-cache), los temporales adicionales se
tienen que mover de la memoria principal al caché. Para M operandos distintos y N operadoes, el
rendimiento es de M/2N de su contraparte en C/F77. Una solución para ello es el uso de Expresión
Templates, los cuales ya los traen varias bibliotecas.
Como hemos visto, existen varias fuentes de bajo rendimiento en C++, es por ello, la importancia de
conocerlas y evitarlas y obtener bibliotecas numéricas que se encuentren optimizadas para así obtener
un buen rendimiento y todas las ventajas de la programación orientada a objetos.
A continuación se darán los primeros resultados que se obtuvieron al corren el programa DFT++ en la
máquina Berenice32 (Origin 2000) de la UNAM.
1600
1400
1200
1000
Tiempo (segundos)
miser op
800 interactivo op
miser no op
600
400
200
0
0 1 2 3 4 5 6 7 8 9
No. procesadore s
74
Como podemos ver en la anterior gráfica, el programa se ejecutó de 1 a 8 procesadores sin optimizar
utilizando una cola llamada miser que me permite apartar todos los recursos necesarios, es decir,
procesadores y memoria; también se ejecutó optimizado mediante la optimización automática del
compilador utilizando el nivel 3 y por último de manera interactiva, es decir, en competencia con los
demás procesos de la máquina con la misma optimización. Como se puede ver, la ejecución interactiva
es la más lenta principalmente al aumentar el número de procesadores. Esto se debe a que tendrá más
competencias por los recursos y que por lo tanto solo utilizará un pequeño porcentaje de ellos.
También podemos ver que el programa optimizado corriendo en la cola miser será el más rápido;
aunque conforme aumenta el número de procesadores, la ejecución del programa no optimizado va
igualando los tiempos del programa optimizado.
Speed Up
op
3
no op
0
0 1 2 3 4 5 6 7 8 9
No. de procesadore s
En ésta gráfica, vemos el speed up de las ejecuciones de los programas que corrieron en la cola miser.
Recordemos que esto es importante ya que así aseguramos de una manera significativa que el ambiente
de ejecución no varíe tanto entre las diferentes ejecuciones. Vemos que el speed up del programa no
optimizado es mejor que el optimizado, debido a que como se vio en la primera gráfica, el programa no
optimizado fue teniendo cada vez mejores tiempo conforme se aumentaba el número de procesadores.
75
Speed Up utilizando la ley de Amdahl
140
120
100
80
op
no op
60
40
20
0
0 20 40 60 80 100 120 140
No. de procesadores
En ésta última gráfica, utilizamos la ley de Amdahl para predecir el speed up de las versiones del
programa hasta 128 procesadores. Claramente se ve que el speed up del programa no optimizado es
mucho mejor que el optimizado.
1600
1400
1200
1000
Tiempo (segundos)
op
800
no op
600
400
200
0
0 20 40 60 80 100 120 140
No. Procesadores
76
Y como vemos en ésta gráfica, una clara consecuencia de los resultados que hemos obtenido es que
con los tiempos teóricos, después de alrededor de 30 procesadores, el programa no optimizado se
ejecutará en un menor tiempo que el programa optimizado, lo que nos dice que algo raro está pasando.
Mflops
350
300
250
200
no op
Mflops
op
in
150
100
50
0
0 1 2 3 4 5 6 7 8 9
No. procesadores
En ésta última gráfica podemos ver los Mflop/s de las tres versiones del programa. Obviamente el
programa que corre de manera interactiva, tiene un número mucho menor de operaciones por segundo
mientras que las versiones no optimizadas y optimizadas ejecutadas bajo miser, prácticamente tienen el
mismo número de operaciones de punto flotante por segundo. Este dato es muy importante, ya que
nos señala que la optimización que se hizo de manera automática, fue solo a nivel de hacer un menor
número de operaciones (como por ejemplo al aproximar valores de las funciones trascendentes) de tal
manera que se ejecutara mas rápido; aunque con una precisión menor; y la optimización no se hizo a
nivel del uso del procesador, como la optimización de ciclos, técnica de pliegue, bloque de caché, etc.
A continuación se darán las conclusiones a las que se ha llegado con solo éstos primeros datos.
6.3 Conclusiones
Con los primeros datos que hemos obtenido, podemos concluir que:
77
Existen operaciones redundantes lo que da lugar al bajo rendimiento observado en los MFlops
(C++). Como hemos visto, esto es algo típico de C++ debido a un diseño no tan acertado.
Gran porcentaje del tiempo de corrida del programa se dedica a librerías matemáticas
(LAPACK y FFTW). Es importante su optimización en la arquitectura donde se corre.
El tiempo de comunicaciones permaneció constante durante las corridas, será interesante ver
su comportamiento en máquinas paralelas como los clusters; ya que,
La optimización del compilador no es suficiente; por lo tanto, se tiene que optimizar a nivel de
código (iguales MFlops).
Como podemos ver, la optimización automática puede ser importante pero la gran mayoría de las veces
la optimización a nivel de código y principalmente un buen análisis y diseño de la aplicación son de
gran importancia para la obtención de buenos rendimientos; principalmente en programas escritos en
C++.
78
APENDICE A
La SGI Origin 2000 es una máquina multiprocesador, escalable hasta con 128 procesadores MIPS
R10000, 4 GB de memoria por nodo (256 GB en total) y 64 interfaces de I/O con 192 controladores
de I/O y de memoria físicamente distribuida y lógicamente compartida mediante la técnica de cc-
NUMA (llamada la arquitectura SN0). Tiene una topología de hipercubo donde cada nodo tiene dos
procesadores, una porción de memoria compartida, un directorio para coherencia de caché el cual es
controlada por el HUB y mantiene el estado del caché de toda la memoria de su nodo el cual es usada
para proveer coherencia de caché y para migrar datos a un nodo que se accesa más frecuentemente que
el nodo presente, es decir, para la migración de páginas que mueve los datos que son frecuentemente
usados a la memoria más cercana al procesador para reducir así la latencia de memoria. Además, los
nodos tienen dos interfaces donde una conecta a los dispositivos de I/O y la otra enlaza los nodos del
sistema a través de la interconexión.
79
Al igual que la memoria, los dispositivos de I/O están distribuidos entre los nodos, pero cada uno esta
disponible a todos los procesadores mediante controladores de I/O e interfaces XIO. También tiene
un controlador de memoria distribuida llamada HUB ASIC e interconexiones CrayLink. Tiene un
crossbow (XBOW) que es un crossbar encargado de conectar dos nodos con hasta seis controladores
de I/O. La fibra de interconexión (CrayLink) es una malla de múltiples enlaces punto-a-punto
conectados por los enrutadores y provee a cada par de nodos con un mínimo de dos trayectorias
distintas. Esta redundancia le permite al sistema eludir enrutadores o enlaces de la fibra que estén
fallando. También, a medida que se agregan nodos a la fibra de interconexión, el ancho de banda y la
eficiencia escalan linealmente sin afectar significativamente las latencias del sistema. Esto es debido a
que se reemplazo el tradicional bus por la fibra de interconexión y porque la memoria centralizada fue
reemplazada por la memoria distribuida pero compartida lógicamente e integrada fuertemente.
En la jerarquía de memoria, sabemos que los registros tienen la más baja latencia, después el caché
primario, que junto con los registros, se encuentra en el mismo CHIP del procesador, tiene una menor
latencia que el caché secundario que se encuentra fuera del CHIP pero que se encuentran en el mismo
nodo. La memoria principal se puede acceder de forma local (en el mismo nodo) y de forma remota
(en otro nodo) y por último, es decir, el de mayor latencia es el caché remoto que existe cuando el
procesador lee caché secundario de otro nodo y se invalida cuando escribe. Por lo tanto, cuando los
procesadores utilizan eficazmente sus memorias caches, el tiempo de acceso a memoria es
insignificante debido a que la gran mayoría de los accesos están satisfechas por la memoria caché.
80
Es por ello, de la importancia de los procesadores MIPS IV R10000, los cuales tienen una arquitectura
superescalar, donde con la técnica de prefetch puede llevar a cabo 4 instrucciones por ciclo y nuestro
caso es de 195 MHz de ciclo de reloj. Tiene un caché primario (L1) de 32KB para instrucciones y
datos y un caché secundario (L2) que puede ser de 512K hasta 16 MB (en nuestro caso es de 4 MB),
además de un bus de caché dedicado y el caché no es bloqueables. Tiene ejecución fuera de orden y
especulativa con branch prediction además de suministrar soporte de hardware para monitorear varios
tipos de eventos. A través de los contadores se puede obtener el rendimiento y localizar cuellos de
botella, permitir afinar una aplicación, predicción del rendimiento de una aplicación y escalabilidad, etc.
El procesador tiene múltiples técnicas para mejorar su rendimiento, entre ellas están:
81
for (i=0;i<N;i++)
{
A[i]=A[i]*B+C;
If (A[i]!=0) /*aquí se interrrumple el flujo del entubamiento*/
x++;
else
y++;
}
Simultáneamente no se puede ejecutar la condición A[i]!=0 y el incremento de x o de y, sino
hasta que se conozca el resultado de la decisión. Bajo éste esquema enfocado a computadoras
superescalares como la Origin 2000 se genera lo siguiente (nivel 3 de optimización):
a) aumento del número de bifurcaciones dada la ejecución simultánea de varias
instrucciones (canalización de software).
b) Aumento de latencia de lecturas constantes a memoria (principal o caché) de la
dirección de la siguiente instrucción de la condicional.
c) Interrumpe el flujo del procesamiento en cascada.
La solución consiste en implementar la predicción de la bifurcación. Esta técnica consiste en
predecir el resultado de una condicional para determinar que camino tomar y ejecutarlo
especulativamente. Es dependiente del procesador, tal como lo es la canalización de software y
la preidentificación de datos. La predicción del camino está basada en un mecanismo de
historia de la ejecución anterior de la condicional. Si anteriormente se guardó que camino se
ejecutó de la condicional, se asume que se tomará el mismo. El procesador posee lo necesario
para la gestión de bifurcaciones de manera eficiente, pero si se hace una predicción errónea, las
instrucciones del camino tomado que están en el entubamiento se abortan y rápidamente se
toma el camino correcto sin afectar sustancialmente el desempeño del programa. Esta técnica
se emplea en la ejecución de cualquier programa sin necesidad de especificar ningún nivel de
optimización, se puede implementar con o sin la canalización de software.
Sucede en los procesadores superescalares que utilizan sus diferentes unidades de ejecución
independientemente para ejecutar instrucciones que se encuentran varias líneas de código
delante de donde se está ejecutando el programa de tal manera que puede completar algunas
instrucciones mientras espera que arriben de memoria operandos de otras.
82
APENDICE B
Jon Bentley es un científico de las ciencias de la computación el cual es conocido por varias
publicaciones que ha hecho, donde una de las más importantes fue “Writing Efficient Programs”. En
éste clásico, Bentley proporcionó una serie de técnicas de optimización que son independientes del
lenguaje y máquina utilizada. Bentley las presentó como una serie de reglas , de las cuales, algunas el
compilador hace automáticamente. Las técnicas son:
Se refiere a la compactación del código. Las expresiones son simplificadas mediante la evaluación del
compilador; por ejemplo, la expresión 8+3+Z+L será transformada a la expresión 11+Z+L. Dentro
de ésta misma técnica, se realiza una propagación de constantes, es decir, se sustituye y rescribe de
forma implícita. Por ejemplo, al evaluar: cte=10.3; valor2=cte/2; se puede sustituir por valor2=10.3/2,
y éste a su ver ser plegado como se mencionó anteriormente. Esta optimización requiere tener
presente las leyes básicas del álgebra, como la conmutativa y asociativa. Es natural que este técnica sea
utilizada en optimizaciones locales, pues internamente un compilador contempla en sus algoritmos de
optimización un módulo de reducción de variables simples, otros de simplificación de operaciones en
asignación, alguno de simplificación de expresiones lógicas y de aritmética.
Se logra una reducción de código, por ejemplo cuando a una variable se le asigna un valor y se reutiliza
varias veces, como es el caso cuando dicha variable es común en otras operaciones. Por ejemplo:
X=A*tg(Y)+(tg(Y)**6), puede ser vista como:t=tg(Y) y X=A*t+(t**6). Existe una redundancia y
puede evaluarse una sola vez en lugar de varias (eliminación de expresiones comunes). Se recomienda
llevar a cabo cuando se trata de evaluar expresiones donde existen dependencias con constantes y
cuando hay operaciones entre índices en vectores o matrices y que comúnmente tienen acceso a
localidades como: abc(i+1)=bci(i+1), etc. Los algoritmos de compiladores deben contemplar casos de
variables simples, constantes, variables índice y expresiones, además de tomar en cuenta la ley
conmutativa y asociativa.
Se refiere a la eliminación de “código inalcanzable”, es decir, que nunca será ejecutado, operaciones
insustanciales, como declaraciones nulas, asignaciones de una variable a sí misma o asignaciones
muertas. En el siguiente ejemplo, el valor asignado a i nunca se utiliza, y el almacenamiento de
memoria para esta variable puede eliminarse. La primera asignación a global también es muerta y la
tercera asignación a global es inalcanzable, ambos pueden ser eliminados.
83
Código original Reemplazada por
Un programa en módulos brinda muchas ventajas, como hemos visto. Se puede alcanzar un tamaño
pequeño de código y una consistencia que permite incrementar la reutilización. No obstante, las
llamadas a funciones pueden ser operaciones relativamente costosas, especialmente si la función es
pequeña. Con esta técnica, se incluye el cuerpo entero de la función en el lugar donde se hace la
llamada. El código en línea puede eliminar la ventaja de utilizar funciones (reducir el tamaño), ya que al
hacer una llamada a una función, el cuerpo es insertado en su lugar; no es recomendable cuando el
tamaño del código se incrementa drásticamente. Algunos compiladores emplean esta técnica sólo
cuando las funciones se han definido dentro del archivo; ya que primero analizan todo el archivo, de
modo que las funciones definidas después del sitio de una llamada puedan hacerse en línea; o bien
pueden hacer funciones en línea que está definidas en archivos separados.
Muchos programas, principalmente numéricos, consumen mucho tiempo de su ejecución en los ciclos,
por tal motivo, los compiladores IRIX hacen énfasis en la optimización de ciclos de una manera
automática (nivel 3), el programador puede indicar esta optimización mediante el uso de algunas
banderas que se describen más adelante. Estas técnicas consisten en hacer transformaciones sobre la
programación de los ciclos, las cuales mejoran el uso del caché y permiten una calendarización de
instrucciones más efectiva (canalizando el software). Algunas técnicas empleadas en la optimización de
ciclos son:
a) Desenrollamiento de ciclos:
Consiste en desenrollar el cuerpo del ciclo dos o más veces incrementando los saltos de ciclo, para
mejorar la reutilización de registros, minimizar recurrencias y de exponer más paralelismo a nivel
instrucción. El número de desenrollamiento es determinado automáticamente por el compilador o
bien por el programador mediante el empleo de directivas. Un ejemplo sería:
84
Código original en C Desenrollamiento
85
b) Fusión de ciclos:
Transforma dos o mas ciclos adyacentes en uno sólo. Permitiendo la reutilización de los datos (que
están en los registros del CPU) y una mejora en el uso del caché (si el ciclo es grande). Por ejemplo, la
fusión de éstos ciclos reduce las instrucciones for y la sincronización requerida, mejorando la eficiencia
y velocidad:
c) Intercambio de ciclos:
Consiste en intercambiar un ciclo inferior por el superior, para mejorar el acceso a memoria. Suele
suceder en Fortran donde el almacenamiento de los datos se hace por columna y la referencia de ellos
se realiza de manera inapropiada (también en C depende de la programación). Por ejemplo:
do i=1,m do j=1,n
do j=1,n do i=1,m
a(i,j)=a(i-1,j)+1.0; a(i,j)=a(i-1,j)+1.0;
enddo enddo
enddo enddo
En el lado izquierdo, si m es un número grande y n un número pequeño, la carga de información de los
datos desde la memoria RAM al caché será muy costoso, debido a que se harán referencias contiguas a
datos alejados, los cuales se encontrarán en diferentes líneas de caché. Por lo tanto, el compilador de
Fortran invertirá el ciclo tal como se presenta en la parte derecha y lo almacenará en memoria.
86
Matriz A
Fortran
En esta imagen, vemos como Fortran y C guardan en los registros del CPU las unidades de la matriz A.
Es muy importante saber esto para hacer los ciclos más eficientes y al momento de pasar partes de la
matriz por una subrutina.
d) Distribución de ciclos:
Consiste en dividir un ciclo en múltiples ciclos, con el propósito de implementar más paralelismo y
canalización de software en la ejecución del programa. Idealmente el ciclo se puede distribuir en un
ciclo secuencial y otra parte en paralelo, donde al ciclo secuencial se le podría implementar el canalizado
de software. Por ejemplo:
for(i=0;i<m;i++) for(i=0;i<m;i++)
{ a[i]=b[i]+c[i]/*ciclo paralelo*/
a[i]=b[i]+c[i]; for(ii=0;ii<m;ii++)
d[i]=d[i+1]+e[i]; d[ii]=d[ii+1]+e[ii]
}
87
El ciclo del lado izquierdo presenta una dependencia de datos en el arreglo d[i], al hacer una referencia
a un valor posterior d[i+1], provocando que no se pueda paralelizar el ciclo completo, por tal motivo el
compilador es capaz de dividir o distribuir el ciclo en dos ciclos: uno que se pueda ejecutar en paralelo y
otro secuencial.
e) Bloqueo de caché:
Técnica del uso del caché, en la cual las matrices que no caben totalmente en el caché se divide en
pequeños bloques, que se ajustan adecuadamente al espacio del caché, evitando la referencia continua a
memoria principal y disminuyendo las fallas de caché (cache misses). Las fallas de caché se presentan
cuando el procesador no encuentra un dato en memoria caché, teniendo que esperar que este dato sea
copiado del caché secundario o de memoria principal. Un ejemplo sería:
donde claramente vemos como al dividir las matrices en submatrices o bloques, se permite cargar los
datos a caché de cada bloque y reutilizarlos cuantas veces sea necesario sin necesidad de leerlos en la
memoria principal.
88
f) reducción de esfuerzo
Consiste en sustituir una operación compleja por una más simple: la multiplicación por la suma o la
diferencia por el cambio de signo. Muchos compiladores harán esto automáticamente. Algunas
instrucciones de máquina son más simples que otras y se pueden utilizar como casos especiales de
operadores más complejos. Por ejemplo, la división o multiplicación de punto fijo por una potencia de
dos es tan simple de implementar como un desplazamiento. La división de punto flotante por una
constante se puede implementar (de manera aproximada) como multiplicación por una constante, lo
cual puede ser más simple. Algunos ejemplos son: el and es más simple que el módulo, multiplicar es
más simple que elevar a una potencia, el corrimiento de bits y la suma es más simple que la
multiplicación.
Las instrucciones de máquina que se ejecutan en cada ciclo de reloj (sin considerer el incremento de
direcciones, ni la bifurcación (Branco), ni el sobreflujo del ciclo) son:
89
Ciclo de reloj Iteración 1 Iteración 2 Iteración 3 Iteración 4 Comentario
h) Preidentificación de datos
La preidentificación de datos es una técnica por la cual el procesador puede solicitar un bloque de
información de la memoria principal al caché secundario sin la necesidad de ocuparlo en ese momento
(lo mismo entre caché secundario y primario). Esto se debe a la capacidad del procesador R10000 de
ejecutar instrucciones en desorden o fuera de orden (out of orden). Los compiladores MIPSPRO
anticipan automáticamente la necesidad de un bloque de datos de la memoria, y lo colocan más cerca
90
del CPU, de modo que cuando sea requerido, el CPU no tenga que esperar la carga del bloque al caché
secundario o primario (reducción de latencia). Se puede emplear el uso de directivas para indicar
manualmente dentro del código (sólo en ciclos) donde se aplicará la preidentificación de datos. Existen
otras técnicas como el uso del TLB y branch predicition, los cuales se vieron en el apéndice A.
91
APENDICE C
REFERENCIA RÁPIDA
92
93
94