You are on page 1of 102

División de Ciencias Básicas

e Ingeniería

Grado: Licenciatura en
Computación

Nombre del alumno: Edmundo


Segundo Carrera Martínez

Matrícula: 96319369

Nombre del asesor: Marcelo Galván Espinosa

Firma: __________________________________________

Fecha de elaboración: Enero de 2002-Mayo 2003


TABLA DE CONTENIDO

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

3. Análisis y diseño de programas paralelos


3.1 Introducción ......................................................................................27
3.2 Análisis del problema: Precondiciones para el paralelismo ..........27
3.3 Análisis del problema: Arquitecturas del problema.......................28
3.4 Análisis del problema: Arquitecturas de las máquinas paralelas ..31
3.5 Análisis del problema: Lenguajes, librerías y utilerías....................33
3.6 Diseño del programa: Introducción................................................34
3.7 Diseño del programa: Partición.......................................................35
3.8 Diseño del programa: Comunicación .............................................36
3.9 Diseño del programa: Agrupación ..................................................38
3.10 Diseño del programa: Asignación ...................................................40
3.11 Juntando los diferentes módulos del programa.............................41

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

Gracias por todo lo que me han dado...

...Siempre estaré orgulloso de ser su hijo.


Con Amor, Respeto y Admiración.
Por ello a Dios y a Ustedes, Gracias.

Edmundo Segundo Carrera Martínez

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.

Sin embargo, aplicar el cómputo de alto rendimiento, requiere de un alto


conocimiento tanto de la aplicación como de las herramientas con las que se
cuenta, para así poder obtener el mayor provecho de ellas y obtener los
resultados más precisos y rápidos posibles. Este trabajo, trata de introducir en
éste fascinante tema a una persona que puede o no tener muchos
conocimientos en el cómputo científico o de alto rendimiento. Para una
mayor investigación, se da la bibliografía necesaria que afortunadamente gran
parte de ella, se encuentra en Internet.

En el primer capítulo, se da una introducción de lo que es el cómputo


científico y de alto rendimiento, los problemas actuales y futuros así como el
rol de las supercomputadoras en las ciencias naturales y de la computación.
En el segundo capítulo, se da un vistazo a las arquitecturas de las
supercomputadoras, lo que nos dará las bases para conocer el ambiente
computacional en el que trabajamos. En el tercer capítulos vemos el análisis y
diseño de los programas paralelos, donde así como es muy importante éste
punto en las aplicaciones seriales; en las paralelas lo es más debido a la gran
cantidad de variables que intervienen y que nos dirán lo eficiente que puede
ser o no una aplicación.

Para apoyarnos más en la eficiencia que puede alcanzar una aplicación


necesitamos algunas métricas (capítulo 4), las cuales pueden ser desde algunas
más sencillas, hasta las más realistas y que necesitan de datos experimentales.
El quinto capítulo nos explica como optimizar nuestra aplicación en
determinada arquitectura, enfocada principalmente en la Origin 2000; debido
a que es la máquina con la que contamos. Sin embargo, al leer todos los
capítulos, no será difícil trasladar ésta información a otra máquina con la que
se cuente.

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.1 Cómputo científico

A menudo se dice que el desarrollo de las computadoras digitales ha transformado el alcance de la


ciencia, porque ello ha originado una tercera metodología de hacer ciencia, detrás de las dos
metodologías tradicionales: la experimental y la teórica; ahora se debe añadir la metodología
computacional. En el área de cómputo científico se ha hecho énfasis en el estudio sistemático de la
utilización de la computadora para resolver una amplia gama de problemas. Recordemos que las
simulaciones por computadora tienen varias ventajas como son:

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.

3. Las posibilidades de cálculo sólo están limitadas por la velocidad de la computadora y la


capacidad de memoria de ésta, mientras que los experimentos físicos tienen muchas
restricciones prácticas.

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?

- ¿Qué herramientas de software existentes ofrecen las mejores expectativas?

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.

Figura 1.1 Interacción entre experimentos, teorías y el cómputo científico en la


resolución de problemas de gran reto.

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.

- Repetición de un evento particular de una simulación.

2
- Estudio de sistemas en los cuales no existe una teoría exacta (métodos heurísticos,
aproximados, etc.)

1.2 Importancia de los métodos numéricos

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:

- El algoritmo es simple, pero el orden de la ecuación o la cantidad de cálculos es muy grande.

- La ecuación es multidimensional.

- Una geometría compleja.

- No se conoce una solución y/o un procedimiento analítico.

- Una solución analítica es posible, pero es complicada y costosa.

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.

Las características de un problema de gran reto son:

- Necesidad de soluciones cada vez de mayor complejidad

a) Gran número de variables, elevados requerimientos de memoria y más capacidad de


procesamiento.

b) Grandes conjuntos de datos requieren un post-procesamiento considerable y/o


visualización muy demandante en software y hardware.

- Tipos de escalas como de longitud y tiempo

a) Escalas demasiado pequeñas o grandes para poder compararlas de forma experimental


como en la simulación de macromoléculas o problemas de cosmología. Picosegundos vs.
Millones de años o nanómetros vs. Años luz.

b) Problemas que debe ser resueltos en tiempo real como por ejemplo el pronóstico del clima.

Los problemas de gran reto se pueden clasificar en cinco grandes categorías:

1. Modelación predictiva y simulaciones.

2. Diseño y automatización de proyectos de ingeniería

3. Exploración de recursos energéticos

4. Investigación médica, militar y básica

5. Investigación social y económica.

Algunos ejemplos de problemas de gran reto son:

- Estudio de la estructura y la dinámica de las moléculas biológicas

- Pronóstico del clima y predicción de los cambios climáticos globales.

- Investigación sobre cómo se crean y evolucionan las galaxias.


4
- Diseño aeroespacial.

- Farmacología.

- Industria automotriz.

- Modelación de superconductores.

- Diseño de nuevos materiales.

- Sistemas biomédicos.

- Proyecto Genoma Humano

- Cromodinámica Cuántica.

- Exploración petrolera.

- Aplicaciones en negocios (transacciones, aerolíneas, simulación macroeconómica, etc.)

1.4 El rol de las supercomputadoras

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.

Sin embargo, imitar a la naturaleza a través de modelos matemáticos requiere de la habilidad de


procesar asombrosas cantidades de datos. Los modelos más complejos podrían requerir de billones de
cálculos que tomarían cientos de horas para su ejecución aún en las supercomputadoras actuales más
rápidas.

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

El supercómputo es una herramienta crítica para el gobierno y la investigación científica y tecnológica,


así como también para aplicaciones comerciales que van desde el diseño automotriz, realidad virtual,
video conferencias, bases de datos paralelas hasta la modelación financiera, incluyendo el análisis de
negocios, entretenimiento, el diagnóstico médico asistido por computadoras, planificación ambiental,
de tráfico y de población, etc.

La relación costo-beneficio de nuevas generaciones de sistemas altamente escalables expandirá el


supercómputo en varias direcciones, siguiendo con la resolución de problemas de gran reto y buscando
nuevos mercados comerciales y aplicaciones. Lo que está guiando la expansión del supercómputo en la
integración de la computación de alto nivel, la visualización y el manejo de grandes volúmenes de
datos. El desarrollo se pude visualizar en diferentes tácticas:

- Cómputo vectorial:

Versiones escalables de supercomputadoras vectoriales jugarán un papel importante en el


futuro del supercómputo, ya que para ciertas clases importantes de aplicaciones, simplemente
no hay sustituto para los vectores (registros y unidades funcionales, principalmente; múltiples
canales de comunicación). La meta es un sistema de vectores (utilizando técnicas pipeline1) con
un rendimiento de múltiples decenas de Teraflops2.

- Cómputo Paralelo:

Aunque inicialmente se tenían computadoras masivamente paralelas (miles de procesadores),


estas tenían procesadores de muy bajos rendimientos y fuertemente acopladas con el resto de
los procesadores, después con los rápidos avances en la tecnología de circuitos integrados (muy
alta escala de integración: VLSI) los procesadores se hicieron cada vez más veloces y las
máquinas paralelas se construyeron con estos procesadores (Pentium, Alpha, MIPS, Sparc,
etc.). En un futuro se vislumbran sistemas híbridos: máquinas paralelas y vectoriales con
capacidad de repartir una aplicación en ambos tipos de máquinas, clusters de computadoras
con comunicación a supercomputadoras y centros de supercómputo acoplados con redes
dedicadas (Grids) para resolver problemas de gran reto.

- 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:

Existen herramientas utilizadas en el cómputo de alto rendimiento que principalmente se usan


para analizar, sintonizar y visualizar el desempeño de una aplicación que se ejecuta en una
máquina vectorial y/o paralela o en un cluster. Algunas de las herramientas que existen son
libres y de dominio público, es decir, que se pueden conseguir en algún sitio de ftp o web y las
hay para distintas arquitecturas.

Bibliografía:

- Tutorial: “Introducción al supercómputo”. DGSCA-UNAM.

- Tutorial: “Paradigmas de la programación paralela”. DGSCA-UNAM.

- Curso: “Programación en paralelo”. CIC-IPN.

- “Arquitectura de computadoras y procesamiento paralelo” Kai Hwang, Fayé A. Briggs. Mc.


Graw-Hill.

7
8
Capítulo 2

ARQUITECTURA DE SUPERCOMPUTADORAS

2.1 Introducción

Es importante el estudio de los dispositivos que componen a las supercomputadoras vectoriales y


paralelas, de memoria compartida y distribuida para entender lo que sucede con el flujo de los datos e
instrucciones entre la memoria, las unidades de procesamiento y los registros. De otra manera se
tendrá dificultad en entender las técnicas de optimización paralelas y vectoriales y la información que
proporcionan las herramientas que realizan algunas optimizaciones de software. Las arquitecturas
avanzadas de computadoras se centran alrededor del concepto de procesamiento paralelo. Para un
óptimo uso de una supercomputadora se deben de tener en cuenta el hardware, software, los
algoritmos utilizados y los lenguajes que se tienen es por ello que damos un rápido vistazo a las
principales arquitecturas y las consecuencias que tienen así como los conceptos necesarios para la
optimización y uso eficiente de las mismas.

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.

Figura 2.1 Crecimiento en la eficiencia de las computadoras en megaflops.

Figura 2.2 Épocas del cómputo.

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.

2.2 Clasificación de las computadoras

Para clasificar a las supercomputadoras es necesario considerar la arquitectura de los procesadores, la


topología de conexión y el rendimiento total de la máquina, no tanto la velocidad de los procesadores.
Una de las clasificaciones más sencillas y fáciles de comprender; aunque actualmente la gran mayoría de
las supercomputadoras entre en varias categorías, es la clasificación de Flynn la cual está basada en la
forma en que se manipulan los datos y las instrucciones de acuerdo a su arquitectura. Las categorías
son:

1. SISD (una instrucción, un dato):

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.

2. SIMD (una instrucción, múltiples datos):

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.

4. MIMD (Múltiples instrucciones, múltiples datos):

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:

1. Arquitecturas RISC (conjunto de instrucciones reducidas de computadora)

Está caracterizada por un conjunto relativamente pequeño de instrucciones simples, ejecución


de instrucciones encadenadas (pipelined), memoria caché, uso de tecnología de compiladores
para optimizar el uso de registros, énfasis en la optimización de la segmentación de
instrucciones, etc. La meta principal de esta arquitectura es la velocidad de ejecución, una
instrucción por cada ciclo de reloj.

2. Arquitecturas CISC (conjunto de instrucciones complejas de computadora)

Esta caracterizada por un conjunto grande y complejo de instrucciones que simplifican el


trabajo de los compiladores y producen programas más pequeño y rápidos (algo que muchas
veces no se cumple).

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:

1. UMA (Acceso uniforme de memoria):

Estas supercomputadoras tienen todos sus procesadores interconectados a través de una


mecanismo de switches a una memoria compartida centralizada. Entre estos mecanismo se
encuentran un bus común, crossbar switches, packet-switched networks, etc. Los sistemas que
usan buses son de tamaño limitado ya que después de cierto número de procesadores el bus se
satura. En el caso de crossbar switches, el costo del switch se convierte en el factor dominante
y limita el número de procesadores.

Figura 2.3 Multiprocesadores de acceso uniforme de memoria.

2. NUMA (Acceso no uniforme de memoria):

En estos multiprocesadores el espacio de direccionamiento es compartido a pesar de que la


memoria es distribuida. Cada procesador tiene cierta memoria local que combinadas forman la
memoria compartida. El tiempo de acceso a memoria depende de si el acceso es local al
procesador o no. El principal problema que presentan las computadoras con ésta tecnología
radica en que no se pueden agregar procesadores indefinidamente ya que a partir de cierto

14
número y dependiendo de la aplicación, el mecanismo de switches o enrutamiento se satura, es
decir, tiene poca extensibilidad.

Figura 2.4 Multiprocesadores de acceso no uniforme de memoria.

3. CC-NUMA (acceso a memoria no uniforme con coherencia de cache):

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.

2.3 Multiplicación pipelined

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.

2.4 Redes de interconexión

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:

- los nodos procesador-memoria

- interfase del nodo a la red

- 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.

Una red está caracterizada por:

- La topología: es la estructura de interconexión física de la gráfica de la red; ésta pude ser


regular, como una malla bidimensional o puede ser irregular. La mayoría de las máquinas
paralelas emplean redes regulares muy frecuentemente. Una distinción es a menudo hecha
entre redes directas las cuales tienen un nodo servidor conectado a cada switch mientras que las
redes indirectas tienen servidores conectados solamente a específicos subconjunto de switches,
que forman las caras de la red. Muchas máquinas emplean una estrategia mixta, de tal forma
que la distinción más crítica es entre los dos tipos de nodos: los servidores generan y eliminan
el tráfico mientras que los switches solo mueven el tráfico hacia delante.

- 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.

- Redes estáticas o dinámicas. En las redes estáticas, la topología de interconexión se define


cuando se construye la máquina. Si la red es dinámica, la interconexión puede variar durante la
ejecución de un programa o entre la ejecución de programas. Entre dos redes, una estática y la
otra dinámica, que ofrezcan el mismo grado de conectividad entre los nodos, la dinámica es
menos costosa de implementar (requiere menos puertos y cableado) pero incrementa el tiempo
promedio que un nodo ha de esperar por una vía de comunicación libre.

Fundamentalmente, hay tres métricas importantes en el rendimiento de la comunicación básica: la


latencia que es el tiempo que se lleva en ejecutar una operación, ancho de banda que es la tasa a la cual
son ejecutadas las operaciones y el costo que es el impacto que estas operaciones tienen en el tiempo de
ejecución del programa. Estas métricas están directamente relacionadas ya que el ancho de banda
(operaciones por segundo) es el recíproco de la latencia (segundos por operación) y el costo es
simplemente la latencia por el número de operaciones realizadas. Ya que una propiedad única de la
arquitectura de las computadoras paralelas es la comunicación, las operaciones que más nos conciernen
son la transferencia de datos.

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.

A continuación veremos algunas de las topologías más importantes:

- Redes totalmente interconectadas: una red totalmente conectada es esencialmente un solo


switch que conecta todas las entradas con todas las salidas. El diámetro es 1 enlace. El grado
es N. La pérdida del switch desconecta toda la red, sin embargo la pérdida de un enlace elimina
solamente un nodo. Una red de este tipo es simplemente un bus. Su costo es de orden O(N).
Desafortunadamente, solamente una transmisión de datos puede ocurrir en cada ocasión, por
lo tanto el ancho de banda total es O(1) Otro ejemplo es un crossbar que provee un ancho de
banda de O(N), pero el costo de la interconexión es proporcional al número de puntos de cruce
o O(N2). Esto indica que no es escalable en la práctica. Esta topología es muy popular en
multiprocesadores de memoria compartida. En principio, la memoria compartida simplifica la
programación paralela ya que no hay que tomar en cuanta la localidad. Sin embargo, la mayoría
de las máquinas paralelas de memoria compartida usan memorias cache para reducir el tráfico
en el bus; por lo tanto, la localidad continua siendo importante ya que el acceso al cache es
mucho más rápido que a la memoria compartida. Un conjunto de máquinas conectadas por
Ethernet es otro ejemplo de una interconexión por bus. Es muy popular para interconectar
estaciones de trabajo y computadoras personales.

Figura 2.6 Interconexión mediante un bus.

- 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.

Figura 2.7 Topologías lineales.

- Mallas y toros multidimensionales: los anillos y arreglos evolucionan naturalmente a grandes


dimensiones incluyendo mallas 2D y toros. Un arreglo d-dimensional consiste de N=kd-1 x···x
k0 nodos, cada uno identificado por sus d-vector de coordenadas. El diámetros de la red es
d(k-1). La distancia promedio es simplemente la distancia promedio en cada dimensión
(2d/3k). Si k es par, la bisección de un arreglo d-dimensional k-ario es kd-1 enlaces
bidireccionales. Las mallas de dos dimensiones y de 3 dimensiones tienen la ventaja de que
pueden ser construidas sin conexiones largas. El diámetro de las mallas puede ser reducido a la
mitad si se extiende la malla con conexiones toroidales de forma que los procesadores en los
bordes también estén conectados con vecinos. Esto sin embargo presenta dos desventajas:
conexiones mas largas y un subconjunto de un torus no es un torus y los beneficios de esta
interconexión se pierden si la máquina es particionada entre varios usuarios. La topología de
malla es adecuada para ejecutar algoritmos orientados a matrices.

Figura 2.8. Malla bidimensional

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.

Figura 2.10 Un árbol binario.

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.

Figura 2.11 Mariposa o Butterfly.

- 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.

Figura 2.12 Pirámide

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.

Figura 2.13. Hipercubos de dimensión 0 a 4.

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:

- Tutorial: “Introducción al supercómputo”. DGSCA-UNAM.

- Tutorial: “Paradigmas de la programación paralela”. DGSCA-UNAM.

- Curso: “Programación en paralelo”. CIC-IPN.

- Curso de “Arquitecturas para el cómputo paralelo” del plan de Becarios de Supercómputo.


DGSCA-UNAM.

Como se explicó durante éste capítulo, el tema de la arquitectura de computadoras y especialmente de


computadoras paralelas es un tema rico en información y con un rápido desarrollo. Para empezar a
conocer la arquitectura de una computadora se recomienda los siguientes libros:

- Organización y arquitectura de computadoras. Diseño para optimizar prestaciones. William


Stallings. Prentice-Hall

- Organización de computadoras. Andrew S. Tanenbaum. Prentice-Hall.

Para el estudio más detallado de supercomputadoras se recomienda los siguientes libros:

- Arquitectura de computadoras y procesamiento paralelo. Kai Hwang, Fayé A. Briggs. Mc.


Graw-Hill

- Advanced Computer Arquitecture. Parallelism, Scalability, Programmability. Kai Hwang. Mc.


Graw-Hill.

- Kai Hwang.

26
Capítulo 3

ANALISIS Y DISEÑO DE PROGRAMAS PARALELOS

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.

3.2 Análisis del problema: Precondiciones para el paralelismo

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í.

3.3 Análisis del problema: Arquitecturas del problema

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:

1. Embarazosamente paralelos: Son aquellas aplicaciones que pueden correrse como


una sucesión de programas seriales, los cuales no necesitan comunicación entre sí;
es decir, los cálculos en cada conjunto de datos (programa serial) son totalmente
independientes. Son los programas más fáciles de tratar y el programa paralelo
ideal.

28
Datos de la Datos de la Datos de la
etapa A etapa B etapa C

Cálculos Cálculos Cálculos,


aplicación aplicación etc.

Resultados
Resultados Resultados

2. Paralelismo pipeline: Son aquellas aplicaciones donde después de determinados


cálculos, se necesita que exista comunicación entre los procesos para seguir con los
cálculos y así sucesivamente. Lo importante de éste tipo de aplicaciones es que los
datos calculados; inmediatamente son requeridos para los siguientes cálculos
después de la comunicación y ya no se necesitarán más. Esto simula una especie de
tubería o cauce de la información la cual puede ser paralelizada cuando distintos
procesos hacen las diferentes etapas siguiendo el modelo de procesamiento
pipeline. Hay que recordar que no todas las etapas tienen la misma complejidad y
por lo tanto el mismo tiempo de cómputo; por eso es importante hacer un buen
balanceo entre las etapas y el poder de cómputo que se tiene; por lo tanto el diseño
del programa paralelo requiere más esfuerzo que el paralelismo embarazoso.

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.

3. Paralelismo totalmente síncrono: Son aquellas aplicaciones donde no existe un flujo


único de la información y los datos; es decir, los cálculos de una región de nuestro
problema afectan en cierto grado todo el espacio del problema esperando así que
terminen todos los cálculos en todas las regiones de nuestro problema para
continuar con la siguiente iteración. Lo importante de éste tipo de aplicaciones es
que los futuros cálculos o decisiones dependen de todos los cálculos hechos
anteriormente. El paralelismo se introduce cuando varios procesos participan en
una iteración haciendo cálculos para diferentes subconjuntos de datos. Aquí es
muy importante la sincronización de los procesos así como un buen balanceo de
carga de trabajo para que no existan tiempos donde varios procesos no hacen
cálculos esperando que terminen otros procesos; es por ello que se necesita un
mayor esfuerzo al momento de paralelizar para obtener un buen rendimiento de las
aplicaciones con éste paralelismo que en el paralelismo pipeline.

Datos iniciales de cada partición

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.

Datos iniciales Datos, más Datos, más


(simples) complejos complejos

Cálculos Cálculos Cálculos,


aplicación aplicación etc.

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.

3.4 Análisis del problema: Arquitecturas de las máquinas paralelas

La arquitectura de la máquina que utilicemos afectará en gran medida el rendimiento y comportamiento


de nuestra aplicación. En las máquinas SIMD la llave es la unidad de control ya que se pueden lograr
grandes rendimientos de programas con el procesamiento pipelined, el estilo de arreglos de operaciones
de Fortran90 o se hagan llamadas a librerias optimizadas para arreglos; de tal manera que el compilador
automáticamente genere el código paralelo optimizado. Para problemas con paralelismo pipelined y

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.

3.5 Análisis del problema: Lenguajes, librerías y utilerías

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.

3.6 Diseño del programa: Introducción

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.

2. Comunicación: Se determina la comunicación requerida para coordinar la ejecución de las


tareas. Se definen estructuras y algoritmos de comunicación.

3. Agrupación: El resultado de las dos etapas anteriores es evaluado en términos de eficiencia y


costos de implementación. De ser necesario, se agrupan tareas pequeñas en tareas más grandes
para mejorar el rendimiento y reducir los costos de desarrollo.

4. Asignación: Cada tarea es asignada a un procesador tratando de maximizar la utilización de los


procesadores y de reducir el costo de comunicación. La asignación puede ser estática o en
tiempo de ejecución mediante algoritmos de balanceo de carga.

34
3.7 Diseño del programa: Partición

En la etapa de partición se buscan oportunidades de paralelismo, evitar reproducir cómputo y datos y


se trata de subdividir el problema lo más finamente posible, es decir, que la granuralidad sea fina. Con
esto, se obtendrá una mayor flexibilidad en términos del programa paralelo potencial ya que en las
demás etapas, debido a los costos de implementación, puede provocar que aglomeremos las tareas y
descartemos ciertas posibilidades de paralelismo. Una buena partición divide tanto los cómputos como
los datos. Hay dos formas de proceder con la descomposició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.

2- Hay que evitar cómputos y almacenamientos redundantes; de lo contrario el algoritmo puede


ser no extensible a problemas más grandes.

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.

5- Considere alternativas de paralelismo en ésta etapa ya que pueden flexibilizar etapas


subsecuentes. Recordar que es mejor usar las técnicas de descomposición funcional y de
dominio para lograrlo.

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.

3.8 Diseño del programa: Comunicación

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.

La comunicación se puede categorizar de la siguiente manera:

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.

4. Comunicación no estructurada: donde la red de comunicación forma un grafo arbitrario.

5. Comunicación estática: donde la identidad de compañeros de comunicación no cambia con el


tiempo.

6. Comunicación dinámica: donde la identidad de compañeros de comunicación puede ser


determinada por los datos computados al momento de ejecutarse la aplicación y puede ser muy
variable. Muy útil cuando existen regiones críticas de comunicación y/o cómputo, donde
existen algoritmos que crean tareas (por lo regular hilos) o crean un balance de carga durante la
ejecución.

7. Comunicación síncrona: donde los productores y consumidores ejecutan su comunicación de


una manera coordinada y la comunicación solo se da cuando ambos cooperan en la operación
de transferencia de datos y ambas tareas no hacen ninguna otra operación.

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:

a) Todas las tareas deben efectuar aproximadamente el mismo número de operaciones de


comunicación. Si esto no se da, es muy probable que el algoritmo no sea extensible a
problemas mayores ya que habrán cuellos de botella.

b) La comunicación entre tareas debe ser tan pequeña como sea posible.

c) Las operaciones de comunicación deben poder proceder concurrentemente para que el


algoritmo sea eficaz y escalable. Si no es así, se puede intentar usar la técnica de divide
y vencerás para lograr la concurrencia.

d) Los cómputos de diferentes tareas deben poder proceder concurrentemente para que el
algoritmo sea eficaz y escalable.

3.9 Diseño del programa: Agrupación.

En las dos etapas anteriores se particionó el problema y se determinaron los requerimientos de


comunicación. El algoritmo resultante es aún abstracto en el sentido de que no se tomó en cuenta la
máquina sobre el cual correrá. En esta etapa se va de lo abstracto a lo concreto y se revisa el algoritmo
obtenido tratando de producir un algoritmo que corra eficazmente sobre cierta clase de computadoras
paralelas. En particular se considera si es útil agrupar tareas para obtener un número menor de ellas y
si vale la pena replicar datos y/o cómputos.

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 comunicación no solo depende de la cantidad de información enviada. Cada comunicación tiene un


costo fijo de arranque. Reduciendo el número de mensajes, a pesar de que se envía la misma cantidad
de información, puede ayudar a mejorar el rendimiento de la aplicación. Así mismo, se puede intentar
replicar cómputos y/o datos para reducir los requerimientos de comunicación. Por otro lado, también
se debe considerar el costo de creación de tareas y el costo de cambio de contexto (context switch) en
caso de que se asignen varias tareas a un mismo procesador.

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.

Los puntos resaltantes que se deben considerar en ésta etapa son:

1. La agrupación debe de reducir los costos de comunicación incrementando la localidad.

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.

5. Si el agrupamiento ha reducido las oportunidades de ejecución recurrente, se debe verificar que


aun hay suficiente concurrencia para máquinas futuras. A veces un algoritmo con insuficiente
concurrencia puede ser más eficiente que otro con excesivos costos de comunicación, modelos
de rendimiento pueden usarse para cuantificar esto.

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.

3.10 Diseño del programa: Asignación.

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:

1. Tareas que pueden ejecutarse concurrentemente en diferentes procesadores para reforzar la


concurrencia o

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:

1. ¿Se ha considerado algoritmos con un número estático de tareas y además algoritmos de


creación y balanceo dinámica de tareas? Muchas veces el sistema computacional es quien se
dedica al balanceo de carga y se le puede ayudar, dándole información sobre el algoritmo que
mejor convenga.

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.

3.11 Juntando los diferentes módulos del programa

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

- buen encapsulamiento de la información el cual reducirá el costo de subsecuentes cambios de


diseño así como facilitará la tarea del entendimiento del problema en general

- uso de herramientas apropiadas, es decir, uso de un paradigma y lenguaje de programación que


permita hacer todo ello y que se adecuen al tipo de análisis, diseño y metodología que se están
haciendo. Los lenguajes modernos como Fortran90, C, C++ y Ada lo permiten hacer, la
decisión también dependerá de la experiencia en programación que se tenga.

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.

En la programación paralela, además necesitamos considerar el número de procesos creados por


módulos, la manera en que cada estructura de datos es particionada y asignada a los procesadores y el
tipo de comunicación que existirá. También se debe pensar como se ejecutarán los módulos, si los
módulos correrán como si fuera un programa secuencial donde diferentes componentes del programa
se ejecutan en secuencia en todos los procesadores (llamándose composición secuencial), permitiendo
así la distribución de los datos y muy útil para el modelo de programación paralela SPMD y ha sido
muy utilizado para la gran mayoría de los proyectos de bibliotecas matemáticas paralelas como
ScaLAPACK; si los módulos correrán concurrentemente en diferentes procesadores (llamándose
composición paralela), lo cual mejorará la escalabilidad y localidad; o si los módulos correrán
concurrentemente en los mismos procesadores (llamándose composición concurrente), con la
ejecución de un módulo particular habilitada por la disponibilidad de los datos lo que puede reducir la
complejidad del diseño y puede permitir la superposición del cómputo y la comunicación.

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:

- Tutorial: “Paradigmas de la programación paralela”. DGSCA-UNAM.

- Curso: “Programación en paralelo”. CIC-IPN.

- Curso de “Paralelismo I” del plan de Becarios de Supercómputo. DGSCA-UNAM.

- 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

Para ver problemas y soluciones concernientes a la comunicación y sincronización de procesos; es


conveniente ver cualquier libro de sistemas operativos, también algunos detalles se encuentran en libros
de sistemas distribuidos como: Distributed Systems. Concepts and Design. George Coulouris.
Pearson Education. Como se planteó en el capítulo anterior, existen básicamente dos modelos de
programación paralela. Otra clasificación puede ser:

- Modelos de programación de memoria distribuida: Cuyas principales herramientas son: MPI


(MPI: The complete Reference. Snir, M., Dongarra, J., y otros. MIT Press, Massachussets.
1996. http://www.mcs.an.gov/mpi/index.html) y PVM (PVM: Parallel Virtual Machina. A
User’s Guide and Tutorial for Networked Parallel Computing. MIT Press, Massachussets.
1994. http://www.netlib.org/pvm/book/pvm-book.html)

43
- Datos paralelos: HPF

- Modelos de programación de memoria compartida: OpenMP

- Modelos híbridos: MPI con OpenMP.

Las tendencias de los códigos paralelos usando diferentes modelos de programación son:

Modelos de programación 1997 2001

Basado en compiladores 72% 64%


(automáticas o con
directivas). Por ejemplo
OpenMP

Basado explícitamente en 44% 59%


compiladores. Por ejemplo
HPF.

Explícitamente en envío de 72% 68%


mensajes. Por ejemplo MPI,
PVM, LINDA.

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:

- Tiempo de ejecución de CPU para:

a) operaciones de registros.

b) operaciones de enteros.

c) operaciones de punto flotante.

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

- Tamaño y divisiones de la memoria.

- Sistemas de archivos.

45
- Compiladores.

- Entrada y salida de datos.

- Comunicación, especialmente respecto a las computadoras paralelas, etc.

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.

Diremos que un algoritmo paralelo eficiente requiere un número polinómico de procesadores, y un


tiempo polilogarítmico.

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

Figura 1.1 Diferentes estimaciones de la ganancia de velocidad de un sistema de


n-procesadores con respecto a un procesador único.

La cota inferior es conocida como la conjetura de Minsky. Aplicando la conjetura de Minsky, la


ganancia de velocidad conforme n aumenta, se vuelve más pesimista. Una estimación más optimista de
la ganancia de velocidad está limitada superiormente por la expresión n/Ln n, la cual se deduce así:
consideremos un problema de computación que pueda ser ejecutado por una computadora
uniprocesador en la unidad de tiempo, T1=1. Sea fi la probabilidad de asignar el mismo problema a i
50
procesadores que trabajen equitativamente con una carga promedio di=1/i por procesador.
Supongamos además equiprobabilidad de cada modo de operación que emplea i procesadores, es decir,
fi=1/n, para n modos de operación: i=1, 2, …, n. El tiempo medio necesario para resolver el
problema en un sistema n-procesador viene dada por la expresión:

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.

4.4 Ley de Amdahl

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

De ésta forma, si el número de procesadores masivamente paralelos es muy grande, entonces en el


1 1
límite pà ∞ tenemos que Sp= = como lo habíamos comentado.
1−α s

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.

Una de las implicaciones de la ley de Amdahl es que el rendimiento es sensitivo al porcentaje


paralelizable y al número de procesadores y con ésta ley se pueden hacer mejores predicciones teóricas
del comportamiento del programa en un número de procesadores con las que actualmente no
contamos, además de que podemos hacer un esfuerzo para mejorar el algoritmo reduciendo el
porcentaje del componente secuencial. Otro aspecto importante es que con ésta ley se puede ver hasta
donde conviene utilizar n número de procesadores ya que cuando uno se acerca al speedup máximo (la
asíntota en la gráfica), no vale la pena correr el programa con más procesadores y entonces utilizar
éstos para otros procesos. Como se ve en la tabla, incluso aplicar un número infinito de procesadores,
solo logrará un speedup de 30. En las gráficas y en la tabla se asume un α=96.77. Por lo tanto, es muy
recomendable tener un algoritmo o aplicación que tenga un alfa mayor o igual a 95% para tener un
buen speedup, recordando que por lo regular éste será una cota superior; ya que el speedup real, será
menor debido a la carga de la máquina, las comunicaciones, la sincronización, etc.

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.

4.6 Modelos mas realistas

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.

Figura 2.1 1 Actividad de 8 procesadores durante la ejecución de un programa


paralelo. T es el tiempo total de ejecución.

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

No siempre el tiempo de ejecución es la métrica más conveniente para evaluar el rendimiento de un


algoritmo paralelo, debido a que éste varía fácilmente con el tamaño del problema. Una manera de
normalizar el tiempo de ejecución al tamaño del problema es con la eficiencia, el cual es la fracción de
tiempo que los procesadores utilizan para hacer el trabajo de cómputo. La eficiencia relativa se define
T
como: E relativo = 1 donde T1 es el tiempo de ejecución en un procesador y Tp es el tiempo en P
PT p
procesadores. Entonces el Speedup relativo se define como Srelativo=PErelativo. Estos valores son
relativos debido a que son definidos con respecto al algoritmo paralelo que se ejecuta en un procesador
y estas métricas son usuales porque exploran la escalabilidad del algoritmo. La eficiencia y speedup
absolutas se obtienen cuando se compara con respecto al tiempo de ejecución mejor conocido en un
solo procesador, donde muchas veces éste tiempo se debe a un programa o algoritmo serial.

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.

La escalabilidad de un algoritmo, se puede cuantificar también con la eficiencia y speedup relativa


incrementando los procesadores para un tamaño de problema fijo. Por lo regular, se llegará a un
número máximo de procesadores para un tamaño fijo de problema. Un dato importante es variar el
tamaño del problema (la carga de cómputo) con respecto al número de procesadores para mantener la
eficiencia relativa constante. A ésta función que se obtiene se le llama función de isoeficiencia; en
donde un algoritmo con una función de isoeficiencia de orden Ο(p) es altamente escalable, no así una
T T1
cuadrática o exponencial. Recordando que E relativo = 1 = p −1 p −1 p −1
,
PT p
∑ Tcomputación + ∑ Tcomunicación + ∑ Tocio
i

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.

4.8 Datos empíricos

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:

- el algoritmo es no determinístico, es decir, usan números aleatorios o exploran un espacio de


búsqueda, etc. En caso de usar números aleatorios, los datos se pueden controlar si se usa un
generador paralelo reproducible de números aleatorios. En caso de exploración de espacio de
búsqueda, la reproducibilidad puede ser más difícil de encontrar; se puede hacer con un
número grande de ensayos o normalizando el tiempo de ejecución dividiéndolo por alguna
métrica de la cantidad de trabajo hecho.

- un cronómetro inexacto, debido a falta de resolución o a que el cronómetro no es exacto. En


caso de que la resolución no sea buena, se puede mejorar aumentando el tiempo de cómputo
del programa ya sea que se aumente el tamaño del problema o el mismo problema varias veces.
En caso de que el cronómetro sea inexacto en sí, se necesitará encontrar un cronómetro u otra
manera de determinar el tiempo que sea más preciso.

- 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

Un importante determinante del comportamiento de muchos programas paralelos es el tiempo


requerido para mover los datos entre las diferentes jerarquías de memoria, es decir, el tiempo requerido
para las entredas/salidas ( input/outputàI/O). Aplicaciones con requerimientos sustanciales de I/O
son:

- 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.

- Los datos de la simulación: simulaciones científicas y de ingeniería por lo regular generan


cantidades muy grandes de datos (cientos de gigabytes o más en una sola corrida), por lo tanto
es muy importante la planificación de los I/O en éstos programas.

- 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.

- recordando la jerarquía de la memoria de la máquina, es a menudo mejor realizar una


redistribución de datos explícita en la memoria, es decir, ocupar mucho más las
comunicaciones entre los procesadores que los I/O, debido al alto costo que tienen.

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.

- nosotros después refinaremos la actuación y conducta de los algoritmos escogidos mediante la


obtención de diferentes métricas que nos dirán que tan bien hemos cumplido con los requisitos
planteados tales como tiempo de ejecución, costos de comunicación, etc.

- 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:

- Tutorial: “Introducción al supercómputo”. DGSCA-UNAM.

- Curso: “Programación en paralelo”. CIC-IPN.

- Curso de “Paralelismo I” del plan de Becarios de Supercómputo. DGSCA-UNAM.

- Fundamentos de Algoritmia. G. Brassard y P. Bratley. Prentice-Hall.

- 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

¿CÓMO OPTIMIZAR UN CÓDIGO?

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:

1. el grado de paralelismo inherente de la aplicación

2. la arquitectura de la máquina paralela en donde la aplicación se ejecutará

3. lo eficaz que el lenguaje y el sistema de ejecución explote la arquitectura


(disponibilidad y velocidad del CPU, instrucciones de entrada y salida, tamaño y
disponibilidad de memoria, etc.)

4. lo eficaz que el código explote al lenguaje, sistema de compilación y


arquitectura (erroresàbugs, usar apropiados algoritmos y estructuras de datos,
bibliotecas matemáticas y/o de comunicación optimizadas, etc.)

5. el ambiente de ejecución en el momento en que se dé (herramientas para el


análisis de rendimiento y actuación, colas, etc.)

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.

5.2 Aspectos a considerar del punto flotante

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.

5.3 Opciones de optimización en la Origin 2000

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

-O0 No hay optimización por parte del compilador


-O1 Se realizan optimizaciones a nivel de bloques de código. Se aplican diversas técnicas
sobre instrucciones del programa. En este nivel la generación del código objeto es
rápido. No se minimiza el acceso a memoria y no se usan registros para variables más
usadas.
-O2 Se hacen optimizaciones extensivas a nivel local.
-O3 Se realiza una optimización agresiva. Es un nivel donde la optimización puede arrojar
resultados erróneos sobre todo en código dentro de ciclos y de operaciones de números
flotantes.
En cada nivel se realizan las siguientes optimizaciones:

Nivel Optimizaciones

-O0 No hay optimizaciones


-O1 Simplificación algebraica.
Eliminación de código muerto.
Propagación y pliegue de constantes. (Constant propagation and holding)
Expansión de arreglos. (Array expansion)
Eliminación de subexpresiones comunes.
-O2 Técnicas del primer nivel.
Propagación global de constantes.
Optimización del flujo del programa (quitando código redundante, borrando secciones
inalcanzables en el programa, combinando diferentes bloques del programa en un solo
bloque largo)
Reducción de esfuerzo. (Strength Reduction)
Simplificación de la variable de inducción.
Conversiones de If.
Iteraciones cruzadas. (Cross-Iteration)
Sustitución hacia atrás (Back substitution)
-O3 Técnicas del segundo nivel.
Aspectos a considerar del punto flotante.
Canalización de software (Software Pipelining)
Preidentificación de datos (Data Prefetching)
Análisis interprocedural. (IPA)
Optimización de ciclos anidados (LNO) que incluye intercambio, distribución, fusión,
desenrollamiento de ciclos y bloqueo de caché.

66
Las opciones para las optimizaciones de ciclos anidados son:

LNO Opción Descripción

Intercambio de ciclos -LNO:interchange:off Desactiva el intercambio de ciclos


-LNO:interchange:on Activa el intercambio de ciclos. Opción
por omisión
Distribución de ciclos -LNO:fission=0 Desactiva la distribución de cilos.
-LNO:fussion=[1,2] Activa la fusión de ciclos. Por omisión –
LNO:fussion=1
Desenrollamiento de -LNO:outer_unroll=n Desenvuelve todos los ciclos los cuales
ciclos pueden desenvolverse exactamente en n.
Bloqueo de caché -LNO:blocking=off Previene el bloqueo de un módulo o de un
ciclo anidado.
-LNO:blocking_size=[l1,l2] Especifica el tamaño de bloque para cache
L1, caché L2 o ambos.
Preidentificación de -LNO:pfn=off Activa o desactiva la preidentificación para
datos el caché de nivel n.
-LNO:prefetch=n Fija un nivel de preidentificación, ninguno,
normal o agresivo.
-LNO:prefetch_manual Valida o invalida la preidentificación
manual.
Las directivas de optimización de ciclos anidados son:

LNO En C En Fortran Descripción

Intercambio de #pragma no C*$ NO Especifica que no se desea un


ciclos interchange INTERCHANGE intercambio de ciclos.
#pragma C*$* INTERCHANGE Especifica que se desea un
interchange (i,j,[k…]) intercambio de ciclos.
(i,j,[k…])
Distribución de #pragma no C*$* NO FISSION Especifica que no se desea una
ciclos fission distribución de ciclos.
#pragma fission C*$* FISSION Especifica que se desea una
distribución de ciclos.
Fusión de ciclos #pragma no C*$* NO FUSSION Especifica que no se desea una
fussion fusión de ciclos.
#pragma C*$* FUSSION Especifica que se desea una
fussion fusión de ciclos.

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.

-mips4 Compila código para el CPU R10000.

-O2 Solicita el mejor conjunto de optimizaciones conservativas, esto es que no


reordenará declaraciones y expresiones.

- Permite optimizaciones que favorecen la velocidad, preservando la


OPT:IEEE_arithmetic=3 precisión numérica de acuerdo con las reglas establecidas para esto.

-lfastm Liga las rutinas usuales matemáticas de las correspondientes bibliotecas


-lm optimizadas.

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.

Entre las ventajas que se tienen utilizando ésta metodología son:

- la modularidad ayuda a la paralelización y optimización

- un alto nivel de abstracción que permitirá la reutilización de código

- un mejor mantenimiento a un menor costo

- reducción del código a través de algoritmos genéricos

- más entendible a nivel de usuario.

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:

- la reducción de la complejidad a través de las jerarquías tiende a reducir la eficiencia.

- el uso de templates, como en la STL (Estándar Template Library) provee mucha generalidad,
flexibilidad y eficiencia.

- el uso de bibliotecas que utilizan ésta tecnología y que se encuentren optimizadas


(MTLàGeneric Programming for Numerical Linear Álgebra).

- 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.

- evitar lo mayor posible el polimorfismo dinámico (funciones virtuales).

- y lo más importante, obtener un buen diseño (algoritmos y TDAs, descomposición del


problema, etc.) e implementación (arquitectura del sistema, bibliotecas y optimizaciones 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.

6.2 DFT++: Primeros resultados

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.

Tiempo vs. No. procesadores

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.

Prediciendo tiempos con ley de Amdahl

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:

Con las gráficas se ve que la optimización no se da a nivel de uso de procesador sino en la


realización de menos operaciones (Mflops vs. tiempoàC++).

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 relación entre tiempo de comunicación y tiempo de procesamiento se vuelve más


importante en el código optimizado, lo que da lugar a un speed up menor.

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 ORIGIN 2000 Y LOS PROCESADORES MIPS R10000

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.

Figura 1. Arquitectura SN0

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.

Tiene 5 unidades de ejecución independientes, de tipo pipeline:

1. 1 unidad de carga y almacenamiento no bloqueable.

2. 2 ALUs de 64 bits para enteros.

3. 1 unidad de suma para punto flotante de 32-64 bits.

4. 1 unidad de multiplicación para punto flotante de 32-64 bits.

El procesador tiene múltiples técnicas para mejorar su rendimiento, entre ellas están:

- ejecución especulativa con predicción de la bifurcación (Branch prediction):


Muchos programas al momento contienen decisiones que generan sólo un camino a tomar por
el procesador de su ejecución. En un esquema de entubamiento o canalización de software de
instrucciones, esto se complica dado que si una instrucción es una decisión o bifurcación
condicional, primero se tiene que esperar a que se ejecute ésta y conocer el resultado para que
le procesador pueda ejecutar la siguiente instrucción. Un ejemplo es:

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.

- ejecución fuera de orden (out of orden):

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

TÉCNICAS DE OPTIMIZACIÓN (LAS REGLAS DE BENTLEY)

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:

1. Técnica de pliegue (Holding):

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.

2. Eliminación de expresiones redundantes:

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.

3. Eliminación de código muerto

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

int global: int global;


void f() void f()
{ {
int i; global=2;
i=1; /*código muerto*/ return;
global=1;/*código muerto*/ }
global=2;
return;
global=3;/*inalcanzable*/
}

4. Funciones en línea (inlining)

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.

5. Optimización de ciclos anidados (LNO)

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

for (i=0;i<24;i++) for (i=0;i<24;i+=4)


{ {
v[i]=v[i-2]+X*w[i]; v[i]=v[i-2]+X*w[i];
} v[i+1]=v[i-1]+X*w[i+1];
v[i+2]=v[i]+X*w[i+2];
v[i+3]=v[i+1]+X*w[i+3];
}
Cuando la variable índice no es múltiplo del incremento, entonces el desenrollamiento de ciclos se hará
de la siguiente manera:

Código original en C Desenrollamiento

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


{ {
a[i]=b[i]; a[i]=b[i];
} }
for (j=0;j<(n/4);j++)
{
a[i]=b[i];
a[i+1]=b[i+1];
a[i+2]=b[i+2];
a[i+3]=b[i+3];
i+=4;
}
Si los límites del ciclo son constantes y el número de iteraciones es pequeño, el ciclo puede ser borrado
totalmente y reemplazado por copias del cuerpo del ciclo.

Código original en C Desenrollamiento

for (i=1;i<5;i++) a[1]=b[1]/a[0];


{ a[2]=b[2]/a[1];
a[i]=b+[i]/a[i-1]; a[3]=b[3]/a[2];
} a[4]=b[4]/a[3];
En los problemas de cómputo que convergen en un cierto resultado, los compiladores rechazan el
desenrollamiento considerando que éste alterará el resultado. Un ciclo desenrollado es más grande que
la versión original, pudiendo ser más grande que el caché de instrucciones (L2), lo cual provocaría que
la versión desenrollada sería mas lenta, por lo tanto hay que tener cuidado en desenrollar el ciclo de tal
manera que utilice eficientemente el caché de instrucciones y de datos.

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ódigo original en C Fusión de ciclos

for (i=0;i<m;i++) for(ij=0;ij<m;ij++)


{ a[i]=b[i]+c[i];} {
for(j=0;j<m;j++) a[ij]=b[ij]+c[ij];
{d[j]=a[j]+e[j];} d[ij]=a[ij]+e[ij];
}
En la parte izquierda está el arreglo a[i], que aparece en ambos ciclos, puede optimizarse combinando
ambos ciclos en uno solo, como aparece en el lado derecho, permitiendo que los elementos del
arreglo[ij] puedan estar disponibles inmediatamente para usarse en la siguiente instrucción y reutilizar
los datos a[ij] almacenados en el registro.

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:

Código original en Fortran Intercambio de ciclos

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:

Código original en C Distribución de ciclos

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.

g) Canalización de software (Software Pipelining)

La arquitectura superescalar del procesador R10000 es capaz de iniciar de manera simultánea


instrucciones de máquina y ejecutarlas en forma independientes (paralelismo a nivel instrucción), hasta
ejecutar cuatros instrucciones de máquina en un solo ciclo de reloj. Actualmente la mayoría de los
procesadores son superescalares. La canalización de software es una técnica de calendarización del
compilador, que llena óptimamente las ranuras de tiempo del procesador superescalar, de tal forma que
cada ciclo de reloj no este desocupado (llegando a tener hasta 4 instrucciones de máquina por ciclo).
Esta técnica se aplica solo en los ciclos del programa ocasionando que se incremente el tiempo de
compilación (depende de lo programado). Traslapa iteraciones sucesivas del ciclo, esto es, se ejecutan
instrucciones en forma simultánea de diversas iteraciones de tal forma que una iteración puede empezar
a ejecutarse mientras que otra está finalizando (instrucciones procesadas en cascada o entubadas).
Además la canalización de software calendariza óptimamente en qué ranura o ciclo de reloj se
ejecutarán las instrucciones de máquina de cada iteración. Por ejemplo en

for (i=0;i<N;i++) {A[i]*B+C;}

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:

Ciclo de reloj Instrucción Comentario

1 LOAD Lee A[i]


2 MUL Multiplica A[i] por B
3 ---- 2 ciclos para la multiplicación
4 ADD Suma C a A[i]*B
5 ---- 2 ciclos para la suma
6 STORE Escribe A[i] a memoria
En total son 6 ciclos de reloj para ejecutar cada iteración. Implementando la canalización de software
con sólo 4 iteraciones de las N en total:

89
Ciclo de reloj Iteración 1 Iteración 2 Iteración 3 Iteración 4 Comentario

1 LOAD ---- ---- ---- Lee A[i]


2 MUL ---- ---- ---- A[1]*B
3 ---- LOAD ---- ---- Lee A[2]
4 ---- MUL ---- ---- A[2]*B
5 ADD ---- LOAD ---- A[1]*B+C y Lee A[3]
6 ---- ---- MUL ---- A[3]*B
7 ADD LOAD A[2]*B+C y Lee A[4]*B
8 STORE ---- ---- MUL Escribe A[1] y A[4]*B
9 ---- ---- ADD ---- A[3]*B+C
10 ---- STORE ---- ---- Escribe A[2]
11 ---- ---- ---- ADD A[4]*B+C
12 ---- ---- STORE ---- Escribe A[3]
13 ---- ---- ---- ----
14 ---- ---- ---- STORE Escribe A[4]
Como se puede observar, con 14 ciclos de reloj se ejecutarán 4 iteraciones simultáneamente. La técnica
de canalización de software implementa el desenrrollamiento de ciclos para obtener beneficios de este
tipo de optimización. La situaciones donde no es conveniente implementar la canalización de software
es cuando una iteración es dependiente de la otra, es decir, cuando existe dependencia de datos en el
ciclo, cuando las iteraciones son muy pequeñas (menor de 10) para canalizaciones mayores de 4,
cuando existen llamadas a procedimientos dentro del ciclo y cuando existen dos variables diferentes
que hacen referencia a la misma dirección en memoria (aaliasing con apuntadores).

h) Preidentificación de datos

Inicialmente los procesadores cargaban instrucciones y datos directamente de la memoria en sus


registros, ocasionando que el procesador no trabajara a la velocidad a la que fue diseñado, dado el
retardo (latencia) que se generaba al estar leyendo directamente a memoria. Con la implementación de
un caché secundario (L2), intermediario en la transferencia de información entre el procesador y la
memoria principal, se logró reducir dicha latencia. Este caché secundario guarda un intervalo de
direcciones, que son un subconjunto de direcciones de la memoria principal, y permite que datos e
instrucciones de la aplicación estén ahí; el acceso a la memoria principal es excepcional, lo cual mejora
la velocidad de ejecución de nuestros programas. Del mismo modo para acelerar aún más el
rendimiento de nuestra aplicación, se ha implementado el uso de caché primario (L1) que reside en el
propio procesador (on-chip caché) y actúa de intermediario entre el caché secundario y el procesador,
las instrucciones y datos se colocan de la memoria principal al caché secundario y del caché secundario
al caché primario, lo que permite la utilización de la técnica de 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

You might also like