Logo Passei Direto

Computadores Paralelos

User badge image
tamarac71

en

Material
¡Estudia con miles de materiales!

Vista previa del material en texto

COMPUTADORES 
PARALELOS 
 
 
Computación de Alta Velocidad 
 
 
 
 
 
 
 
 
 
A. Arruabarrena — J. Muguerza 
 
Konputagailuen Arkitektura eta Teknologia saila 
Informatika Fakultatea — Euskal Herriko Unibertsitatea 
 
 
 
 
 
 
 
COMPUTADORES 
PARALELOS 
 
 
Computación de Alta Velocidad 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
A. Arruabarrena — J. Muguerza 
 
Konputagailuen Arkitektura eta Teknologia saila 
Informatika Fakultatea — Euskal Herriko Unibertsitatea 
 
 
 
 
 
 
 
 
septiembre 2011 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ÍNDICE 
 
 
 Introducción ......................................................................... 1 
Capítulo 1. Computadores Vectoriales ............................................... 7 
1.1 ¿Qué es un computador vectorial? ................................................................... 7 
1.1.1 Algunos problemas ............................................................................................... 10 
1.1.1.1 La memoria de un computador vectorial .......................................... 10 
1.1.1.2 Unidades funcionales vectoriales ........................................................ 11 
1.1.1.3 Registros vectoriales ............................................................................... 11 
1.1.1.4 Programas vectoriales ............................................................................ 12 
1.1.2 Arquitectura y lenguaje-máquina ...................................................................... 13 
1.2 Dependencias de datos ..................................................................................... 15 
1.2.1 Encadenamiento (chaining) ........................................................................................... 16 
1.2.1.1 Encadenamiento con dos instrucciones ............................................ 17 
1.2.2 Tablas de ejecución de las instrucciones ........................................................ 18 
1.3 Dependencias estructurales .............................................................................. 20 
1.3.1 Buses de memoria (unidades funcionales LV/SV) ........................................ 20 
1.3.2 Conflictos en el uso de los módulos de memoria ........................................ 22 
1.3.2.1 Una sola operación de memoria ......................................................... 22 
1.3.2.2 Varias operaciones de memoria .......................................................... 26 
1.3.3 Longitud de los registros vectoriales (strip mining) ...................................... 29 
1.4 Velocidad de cálculo de los computadores vectoriales ............................ 30 
1.4.1 Velocidad de cálculo en función de la longitud de los vectores .............. 31 
1.4.1.1 R∞ y N1/2..................................................................................................... 31 
1.4.1.2 Speed-up o factor de aceleración ....................................................... 33 
1.4.1.3 Nv ................................................................................................................. 34 
1.4.2 Influencia del código escalar. Ley de Amdahl. .............................................. 34 
1.5 Técnicas de compilación para generar código vectorial ........................... 37 
1.5.1 Dependencias de datos entre instrucciones .................................................. 38 
1.5.2 Vectorización ......................................................................................................... 40 
1.5.2.1 Vectores de una dimensión .................................................................. 40 
1.5.2.2 Vectores de N dimensiones ................................................................. 44 
1.5.2.3 Condición para vectorizar un bucle ................................................... 45 
1.5.2.4 Test de dependencias ............................................................................ 46 
1.5.3 Optimizaciones...................................................................................................... 50 
1.5.3.1 Sustitución global hacia adelante (global forward substitution) ................... 50 
▪ vi ▪ ÍNDICE 
1.5.3.2 Eliminación de las variables de inducción ......................................... 51 
1.5.3.3 Antidependencias (DR, WAR) ...................................................................... 52 
1.5.3.4 Dependencias de salida (RR, WAW) ........................................................ 53 
1.5.3.5 Intercambio de bucles (loop-interchanging) .......................................... 54 
1.5.3.6 Expansión escalar (scalar expansion) ........................................................ 56 
1.5.3.7 Fusión de bucles (loop fusion) ..................................................................... 57 
1.5.3.8 Colapso de bucles (loop collapsing) ......................................................... 58 
1.5.3.9 Otras optimizaciones ............................................................................. 59 
1.5.4 Vectores de máscara y vectores de índices ................................................... 60 
1.5.4.1 Uso de máscaras ..................................................................................... 60 
1.5.4.2 Vectores de índices ................................................................................ 61 
1.6 Resumen ................................................................................................................ 64 
Capítulo 2. Computadores Paralelos (conceptos básicos) ................. 69 
2.1 Introducción ......................................................................................................... 69 
2.2 Computadores DM-SIMD ................................................................................. 71 
2.3 Computadores MIMD ........................................................................................ 73 
2.3.1 Memoria compartida (shared memory) .................................................................. 73 
2.3.2 Memoria privada o distribuida (distributed memory) ....................................... 74 
2.3.3 Memoria lógicamente compartida pero físicamente distribuida 
(distributed shared memory) ......................................................................................... 75 
2.3.4 Clusters, constellations... y otros ....................................................................... 76 
2.4 Algunos problemas ............................................................................................. 77 
2.5 Rendimiento del sistema paralelo (leyes de Amdahl y Gustafson) ......... 79 
Capítulo 3. Coherencia de los Datos en los Computadores 
SMP ...................................................................................... 83 
3.1 Presentación del problema y revisión de conceptos .................................. 83 
3.1.1 Coherencia de los datos en los sistemas de un solo procesador ............. 84 
3.1.2 Coherencia de los datos en los multiprocesadores de memoria 
compartida (SMP) ................................................................................................. 85 
3.1.3 Falsa compartición ................................................................................................ 86 
3.1.4 Definición de la coherencia................................................................................ 87 
3.2 Protocolos de coherencia snoopy .................................................................. 88 
3.2.1 Estados de los bloques en la memoria cache y señales de control .......... 90 
3.2.2 Protocolos de invalidación ................................................................................. 93 
3.2.2.1 Un protocolo de tres estados, MSI ..................................................... 94 
3.2.2.2 El protocolo Illinois, MESI ...................................................................... 97 
3.2.2.3 El protocoloBerkeley, MOSI ................................................................ 99 
3.2.2.4 Resumen de los protocolos de invalidación................................... 100 
ÍNDICE ▪ vii ▪ 
3.2.3 Protocolos de actualización ............................................................................. 101 
3.2.3.1 El protocolo Firefly, MSE(I) .................................................................. 101 
3.2.3.2 El protocolo Dragon, MOES(I) ........................................................... 103 
3.2.4 Resumen de los protocolos de tipo snoopy ................................................ 105 
3.3. Implementación de los protocolos snoopy ................................................ 105 
3.3.1 Problemas ............................................................................................................. 105 
3.3.1.1 Directorio de la memoria cache ........................................................ 106 
3.3.1.2 Búferes de escritura .............................................................................. 107 
3.3.1.3 Protocolo de petición de bus ............................................................. 108 
3.3.1.4 Atomicidad: estado del controlador snoopy .................................. 109 
3.3.2 El protocolo Illinois y la atomicidad ................................................................ 110 
3.3.2.1 Carreras: estados transitorios, señales BRQ y BGN ..................... 110 
3.3.2.2 Deadlock, livelock, starvation ............................................................ 112 
3.4 Snoopy jerárquico ............................................................................................. 113 
3.4.1 Lecturas .................................................................................................................. 115 
3.4.2 Escrituras ................................................................................................................ 116 
Capítulo 4. Sincronización de Procesos en los Computado-
res SMP .............................................................................. 119 
4.1 Introducción ....................................................................................................... 119 
4.2 Exclusión mutua (mutual exclusion) ............................................................. 123 
4.2.1 Instrucciones Test&Set y Swap ........................................................................ 125 
4.2.1.1 Instrucción Test&Set ............................................................................. 125 
4.2.1.2 Instrucción Swap ................................................................................... 125 
4.2.1.3 Análisis del tráfico ................................................................................. 126 
4.2.1.4 Procedimiento Test&Set with backoff .............................................. 127 
4.2.1.5 Procedimiento Test-and-Test&Set ...................................................... 128 
4.2.1.6 Resumen de características ................................................................ 130 
4.2.2 Instrucciones Load Locked / Store Conditional y Compare&Swap ....... 131 
4.2.2.1 Instrucciones LL y SC ........................................................................... 132 
4.2.2.2 Instrucción Compare&Swap ............................................................... 134 
4.2.2.3 Algunos problemas con las instrucciones LL/SC ........................... 135 
4.2.3 Instrucciones Fetch&Op .................................................................................... 136 
4.2.4 Alternativas para reducir el tráfico .................................................................. 137 
4.2.4.1 Tickets ...................................................................................................... 137 
4.2.4.2 Vectores de cerrojos ............................................................................ 139 
4.3 Sincronización "punto a punto" mediante eventos ................................... 141 
4.4 Sincronización mediante barreras ................................................................. 142 
4.4.1 Una barrera sencilla ............................................................................................ 142 
4.4.2 Barreras reutilizables .......................................................................................... 143 
4.4.3 Eficiencia ................................................................................................................ 145 
4.5 Resumen .............................................................................................................. 146 
▪ viii ▪ ÍNDICE 
Capítulo 5. Consistencia de la Memoria en los Computa-
dores Paralelos ................................................................ 149 
5.1 Introducción ....................................................................................................... 149 
5.1.1 Sistemas de un solo procesador ...................................................................... 149 
5.1.2 Sistemas multiprocesador ................................................................................. 150 
5.1.3 Semántica de los programas y orden de ejecución de las 
instrucciones ......................................................................................................... 151 
5.1.4 Atomicidad de las instrucciones ...................................................................... 153 
5.1.5 Modelos de consistencia ................................................................................... 154 
5.2 Consistencia secuencial (SC, sequential consistency) .............................. 155 
5.2.1 Orden y atomicidad de las instrucciones de memoria .............................. 156 
5.2.2 Efectos en el hardware y en el compilador .................................................. 158 
5.3 Modelos relajados (relaxed) ........................................................................... 159 
5.3.1 Total Store Ordering (TSO) / Processor Consistency (PC) ....................... 160 
5.3.2 Partial Store Ordering (PSO) ............................................................................ 162 
5.3.3 Modelos más relajados ...................................................................................... 163 
5.3.3.1 Weak Ordering (WO) .......................................................................... 164 
5.3.3.2 Release Consistency (RC) ................................................................... 164 
5.4 Resumen y perspectivas .................................................................................. 166 
Capítulo 6 La Red de Comunicación de los Computadores 
Paralelos. Comunicación mediante Paso de 
Mensajes. .......................................................................... 169 
6.1 Introducción ....................................................................................................... 169 
6.2 Topología de la red ........................................................................................... 171 
6.3 Redes formadas por conmutadores .............................................................. 173 
6.3.1 El conmutador (switch) .................................................................................................. 174 
6.3.2 Red crossbar ......................................................................................................... 175 
6.3.3 Redes multietapa (multistage) .................................................................................... 176 
6.3.3.1 La red Omega ........................................................................................ 176 
6.3.3.2 Encaminamiento en la red Omega ................................................... 178 
6.3.3.3 Conflictos de salida y bloqueos ......................................................... 179 
6.3.3.4 Otro patrón de comunicación: broadcast. ..................................... 181 
6.3.3.5 Otras redes .............................................................................................181 
6.3.3.6 Resumen .................................................................................................. 183 
6.4 Redes formadas por encaminadores de mensajes .................................... 184 
6.4.1 Encaminadores de mensajes ............................................................................ 184 
6.4.2 Topologías de red más utilizadas .................................................................... 185 
6.4.2.1 Redes de una dimensión: la cadena y el anillo .................................. 186 
6.4.2.2 Mallas y Toros (mesh, torus) ....................................................................... 187 
ÍNDICE ▪ ix ▪ 
6.4.2.3 Hipercubos (hypercube) ............................................................................... 188 
6.4.2.4 Árboles y árboles densos (fat tree) ........................................................... 190 
6.4.2.5 Resumen de topologías ....................................................................... 191 
6.4.2.6 Los enlaces físicos ................................................................................. 193 
6.5 La comunicación a través de la red en los sistemas paralelos ............... 193 
6.5.1 Los mensajes ........................................................................................................ 194 
6.5.2 Patrones de comunicación: con quién y cuándo hay que 
efectuar la comunicación. ................................................................................. 195 
6.5.3 Construcción del camino (switching strategy) ................................................... 198 
6.5.4 Encaminamiento de los mensajes (routing) ......................................................... 199 
6.5.4.1 El registro de encaminamiento .......................................................... 200 
6.5.4.2 Elección del camino: estático o adaptativo .................................... 203 
6.5.5 Control del flujo de información ..................................................................... 206 
6.5.5.1 Avance de los paquetes: Store-and-forward, 
Wormhole y Cut-through .................................................................... 206 
6.5.5.2 Conflictos en el uso de recursos: los búferes ................................. 210 
6.5.6 Eficiencia de la comunicación: latencia y throughput ............................... 214 
6.5.6.1 Tiempo de comunicación en la red .................................................. 215 
6.5.6.2 Considerando el tráfico en la red ...................................................... 217 
6.5.6.3 Cálculo del throughput máximo ........................................................ 219 
6.5.6.4 Análisis global ......................................................................................... 221 
6.5.7 Problemas de la comunicación ....................................................................... 222 
6.5.7.1 Deadlock (interbloqueos) Canales virtuales. Giros 
controlados (Turn model). Control de la inyección de 
paquetes. Utilización de caminos seguros ..................................... 222 
6.5.7.2 Problemas de livelock y starvation.................................................... 229 
6.5.8 Protocolos de comunicación ........................................................................... 230 
6.6 Evolución de los computadores paralelos ................................................... 232 
 
Apéndice. Cálculo de las distancias medias en diferentes topologías .......... 235 
Capítulo 7. Coherencia de los Datos en los Computadores 
DSM .................................................................................... 241 
7.1 Introducción ....................................................................................................... 241 
7.2 Directorios de coherencia ............................................................................... 243 
7.2.1 Introducción y clasificación ................................................................................ 243 
7.2.1.1 Problemas................................................................................................ 245 
7.2.2 Estructura de los directorios ............................................................................. 246 
7.2.2.1 Directorios implementados en memoria principal ....................... 246 
7.2.2.2 Directorios implementados en memoria cache ............................ 251 
7.2.3 Optimización del tráfico de coherencia ........................................................ 254 
7.2.4 Atomicidad de las operaciones: carreras ...................................................... 257 
▪ x ▪ ÍNDICE 
7.3 Implementación de los protocolos de coherencia: dos 
ejemplos .............................................................................................................. 259 
7.3.1 Protocolo de coherencia de los multicomputadores SGI Origin ............ 259 
7.3.1.1 Lecturas .................................................................................................... 260 
7.3.1.2 Escrituras .................................................................................................. 263 
7.3.1.3 Actualización de la memoria principal ............................................ 268 
7.3.2 El protocolo de coherencia estándar SCI en la máquina NUMA-Q 
de Sequent. ........................................................................................................... 269 
7.3.2.1 SCI: estados y operaciones ................................................................. 270 
7.3.2.2 Lecturas .................................................................................................... 272 
7.3.2.3 Escrituras .................................................................................................. 273 
7.3.2.4 Actualización de la memoria principal ............................................ 277 
7.3.2.5 Atomicidad y carreras .......................................................................... 277 
7.4 Resumen .............................................................................................................. 279 
Capítulo 8. Paralelización y Planificación de Bucles .................. 281 
8.1 Introducción ....................................................................................................... 281 
8.1.1 Ideas básicas sobre paralelización de bucles ............................................... 287 
8.2. Estructuras básicas para expresar el paralelismo de los bucles .............. 290 
8.2.1 Bucles sin dependencias entre iteraciones: bucles doall .......................... 290 
8.2.2 Bucles con dependencias entre iteraciones ................................................. 291 
8.2.2.1 Bucles forall (sincronización global .................................................. 292 
8.2.2.2 Bucles doacross (sincronización punto a punto) .......................... 293 
8.2.3 Efecto de las antidependencias y de las dependencias de salida ........... 298 
8.2.4 Atención con las instrucciones if ..................................................................... 299 
8.3 Implementación de la sincronización........................................................... 300 
8.3.1 Sincronización mediante contadores ............................................................. 301 
8.3.2 Un único contador por procesador................................................................ 303 
8.4 Optimizaciones para paralelizar bucles de manera eficiente ................. 304 
8.4.1 Eliminación del efecto de las dependencias que no son esenciales ...... 304 
8.4.2 Fisión de bucles ................................................................................................... 305 
8.4.3 Ordenación de las dependencias ................................................................... 306 
8.4.4 Alineación de las dependencias (peeling) .................................................... 307 
8.4.5 Extracción de threads independientes (switching) .....................................309 
8.4.6 Minimización de las operaciones de sincronización ................................. 310 
8.4.7 Tratamiento de bucles (reordenación...) ........................................................ 311 
8.4.7.1 Intercambio de bucles.......................................................................... 311 
8.4.7.2 Cambio de sentido................................................................................ 314 
8.4.7.3 Desplazamientos (skew) ....................................................................... 314 
8.4.7.4 Colapso y coalescencia de bucles .................................................... 315 
8.5 Planificación de bucles (scheduling) ............................................................... 316 
8.5.1 Reparto de las iteraciones: consecutivo o entrelazado ............................. 317 
ÍNDICE ▪ xi ▪ 
8.5.2 Planificación estática o dinámica .................................................................... 318 
8.5.2.1 Planificación estática ............................................................................ 319 
8.5.2.2 Planificación dinámica: autoplanificación (self/chunk 
scheduling), autoplanificación guiada (GSS) y trapezoidal 
(trapezoid self scheduling) ........................................................................... 319 
8.6 Secciones paralelas: Fork / Join ..................................................................... 323 
8.7 Análisis del rendimiento................................................................................... 325 
Capítulo 9. Computadores de Alta Velocidad. Herramientas 
para Programar Aplicaciones Paralelas 
(introducción). ..................................................................... 327 
9.1 Computadores paralelos de alto rendimiento ............................................ 328 
9.2 Herramientas para programar aplicaciones paralelas (introducción) ... 331 
9.2.1 OpenMP ................................................................................................................ 332 
9.2.2 MPI ......................................................................................................................... 336 
 
 
 
 
 
 
Introducción 
 
 
 
 
 
 
 
 
 
 
 
 
¿Qué tiempo hará mañana en esta ciudad? ¿Cómo evolucionan las 
galaxias? ¿Cómo interaccionan los electrones en una molécula de clorofila? 
¿Se comportarán de manera adecuada las alas de un avión en una 
turbulencia? Para dar respuesta adecuada a esas y otras muchas preguntas, 
científico/as e ingeniera/os utilizan potentes computadores, la herramienta 
principal de cualquier laboratorio en la actualidad. Las aplicaciones técnico-
científicas requieren de grandes cantidades de cálculo, casi de manera 
ilimitada, y además hay que obtener resultados en el menor tiempo posible, 
(prever mañana las lluvias torrenciales de hoy no sirve para mucho!). A 
pesar del espectacular incremento en la velocidad de cálculo de los 
procesadores, las necesidades van siempre muy por delante. A lo largo de la 
evolución de los computadores tres han sido las líneas principales que han 
▪ 2 ▪ INTRODUCCIÓN 
permitido aumentar de manera continuada la velocidad de los mismos: los 
avances en la tecnología electrónica, el desarrollo de nuevas estructuras o 
arquitecturas de computadores, y el uso de tecnologías del software 
(compiladores, etc.) cada vez más eficientes. 
Mediante la tecnología electrónica se ha conseguido integrar en un sólo 
chip una cantidad ingente de transistores: hoy en día por encima de 1.000 
millones (y cada vez más). A consecuencia de este avance, cada vez son más 
las "partes" del computador que se van integrando en un solo chip junto con 
el procesador: unidades funcionales específicas, registros, memoria cache... e 
incluso varios procesadores o núcleos (core). Del mismo modo, la frecuencia 
del reloj del computador es cada vez más alta (aunque la carrera para usar 
relojes cada vez más rápidos está detenida en este momento), actualmente en 
el intervalo 1-4 GHz, lo que quiere decir que el tiempo de ciclo está por 
debajo del nanosegundo (F = 1 GHz → T = 1 ns) y, como consecuencia, se 
pueden hacer más operaciones por unidad de tiempo. 
Desde el punto de vista de la arquitectura del sistema, todos los 
procesadores actuales son superescalares o de tipo VLIW (la ejecución de las 
instrucciones es segmentada y se intenta ejecutar más de una instrucción 
cada ciclo); la jerarquía de memoria cache permite accesos más rápidos, los 
registros se organizan para optimizar el uso de los datos, etc. 
Las técnicas de compilación también han avanzado mucho. El objetivo 
principal es eliminar el efecto de las dependencias existentes entre las 
instrucciones, y ocultar la latencia de la unidades funcionales (aprovechando 
ese tiempo para realizar trabajo útil). 
Sin embargo, a pesar de que tenemos procesadores superescalares muy 
rápidos —que llegan a superar la velocidad de cálculo de 10 Gflop/s— para 
muchas aplicaciones, tales como previsiones meteorológicas, simulaciones 
de procesos físicos y químicos, diseños de aeronáutica, prospecciones 
geológicas, diseño de nuevos materiales, desarrollos diversos en ingeniería, 
avances en biología, genética y farmacia, etc., dicha velocidad no es 
suficiente. En el periodo 1986-2002, la tasa de crecimiento del rendimiento 
de los procesadores fue de un %52 anual (!), pero dicho crecimiento se ha 
reducido notablemente estos últimos años, situándose en torno al 20%: la 
velocidad que se puede conseguir con un procesador está llegando a sus 
límites físicos (y económicos). Por tanto, se necesita de desarrollar otro tipo 
de estrategias para conseguir las velocidades de cálculo —Teraflop/s, 
Petaflop/s, es decir, 1012, 1015 operaciones de coma flotante por segundo— 
que demandan las aplicaciones citadas. 
INTRODUCCIÓN ▪ 3 ▪ 
El paso que hay que dar es bastante claro: utilizar muchos procesadores, 
para repartir la ejecución de un programa entre ellos; es decir, utilizar 
sistemas paralelos. Además, las tecnologías de fabricación facilitan esta 
posibilidad: construido un procesador (chip), se hacen fácilmente miles de 
ellos y de manera relativamente barata. Por tanto, ¿por qué no utilizar 100, 
1.000, 10.000... procesadores para resolver un problema? Teóricamente, y si 
supiéramos cómo hacerlo, utilizando P procesadores podríamos ejecutar un 
programa P veces más rápido. Por desgracia, esto no va a ser así, ya que van 
a aparecer importantes problemas nuevos: ¿cómo se reparte el trabajo entre 
los procesadores? ¿son independientes los procesos o hay que 
sincronizarlos? ¿cómo se implementa la comunicación entre procesadores?... 
Existen muchas maneras de estructurar un computador de P procesadores. 
Algunas características serán comunes en todos ellos, y otras, en cambio, no. 
Existen diferentes formas de clasificar estas arquitecturas o estructuras. De 
entre ellas, la más conocida o utilizada es, seguramente, la de Flynn (1966), 
quizás por lo simple que es. En esta clasificación se tienen en cuenta dos 
parámetros: el número de flujos de instrucciones (es decir, el número de PCs 
o contadores de programa) y el número de flujos de datos que operan 
simultáneamente. La siguiente figura recoge dicha clasificación: 
 
 flujos de datos 
 uno muchos 
flujos de 
instrucciones 
uno SISD SIMD 
muchos MIMD 
 
• Computadores de tipo SISD (Single-Instruction-Single-Data) 
 Se ejecuta un único programa sobre un único conjunto de datos; por 
tanto, a esta clase pertenecen los sistemas clásicos de un sólo 
procesador (ordenadores personales, estaciones de trabajo…). Aunque 
en algunos casos dispongan de más de un procesador, éstos realizan el 
trabajo de manera independiente. 
 Como ya hemos comentado, las instrucciones se ejecutan de manera 
segmentada, dividida en varias fases —búsqueda, descodificación, 
lectura de operandos, memoria, unidad aritmética, escritura de 
resultados…—, y en cada fase habrá una instrucción (o varias, en el 
casode los procesadores superescalares). Así pues, se utiliza 
▪ 4 ▪ INTRODUCCIÓN 
paralelismo a nivel de instrucción (ILP, Instruction Level 
Parallelism). Además, el procesador (con ayuda del hardware o del 
compilador) es capaz de modificar el orden de ejecución de las 
instrucciones para conseguir la mayor eficiencia (velocidad) posible. 
 A lo largo del texto supondremos que todos esos conceptos son 
conocidos. 
• Computadores de tipo SIMD (Single-Instruction-Multiple-Data) 
 En este tipo de computadores se ejecuta simultáneamente el mismo 
programa en todos los procesadores, pero sobre diferentes conjuntos 
de datos; se aprovecha, por tanto, el paralelismo de datos (DLP, Data 
Level Parallelism). Dentro de este grupo podemos distinguir dos 
subgrupos: los denominados processor-array (distributed memory 
SIMD) y los procesadores vectoriales (shared memory SIMD). 
 En el primer caso, el computador dispone de muchos procesadores 
normalmente muy "simples" (por ejemplo, 16 k procesadores de un 
bit); todos los procesadores ejecutan el mismo programa de manera 
sincronizada, pero sobre datos diferentes. Se han construido muchas 
máquinas de tipo SIMD, sobre todo en los años 80-95, y para ciertas 
aplicaciones, tales como cálculo numérico, procesamiento de señal, 
etc., ofrecen muy buen rendimiento. 
 Sin embargo, hoy en día no se fabrican computadores de este modelo 
(aunque ideas similares se utilizan para generar entornos virtuales de 
dos y tres dimensiones); sí, en cambio, computadores vectoriales. 
 
• Computadores de tipo MIMD (Multiple-Instruction-Multiple-Data) 
 Es el caso general de un sistema paralelo. Se ejecutan muchos 
procesos (muchos PCs) sobre diferentes conjuntos de datos. ¡Ojo! no 
se trata de un conjunto de máquinas SISD, ya que los programas que 
se ejecutan no son independientes. 
 Este es el modelo que permite obtener elevadas velocidades de 
cómputo: computadores de paralelismo masivo, en los que P 
procesadores (un número alto) colaboran en la resolución de un 
problema; es decir, se explota el paralelismo a nivel de hilo o proceso 
(TLP, Thread Level Parallelism). En cualquier caso, surgen muchos 
problemas nuevos, a los que, si se quiere conseguir un buen 
rendimiento, habrá que buscar soluciones adecuadas. 
INTRODUCCIÓN ▪ 5 ▪ 
 Tal y como veremos en los próximos capítulos, podemos hacer una 
subclasificación en el grupo de las máquinas MIMD: 
 
• Sistemas de memoria compartida, en los que todos los 
procesadores utilizan el mismo espacio de direccionamiento. La 
memoria puede estar centralizada (SMP, symmetric 
multiprocessors) o distribuida (DSM, distributed shared 
memory). La comunicación entre procesos se realiza mediante el 
uso de variables compartidas. 
 
• Sistemas de memoria privada distribuida, en los que cada uno 
de los procesadores utiliza su espacio propio de memoria. LA 
comunicación entre procesos se realiza mediante paso de 
mensajes. 
 
 
A lo largo de los capítulos del texto vamos a analizar las máquinas 
paralelas de tipo MIMD, pero en el primero vamos a tratar sobre un tipo 
especial de computador SIMD de muy alto rendimiento: los computadores 
vectoriales. Se trata de una arquitectura de procesador específica, destinada 
al procesamiento de vectores, que ha conseguido un lugar destacado en la 
historia de la computación. En el capítulo 2 haremos una breve presentación 
de los sistemas paralelos: principales modelos y arquitecturas, problemas 
más importantes, la ley de Amdahl, etc. En el capítulo 3 analizaremos el 
problema de la coherencia de los datos en sistemas SMP; en el 4, las 
instrucciones y procedimientos básicos para sincronizar procesos paralelos: 
T&S, LL, SC...; y en el 5, los modelos de consistencia, secuencial y 
relajados, de la memoria de un sistema paralelo. En el capítulo 6, 
analizaremos la topología, estructura y funcionamiento de la red de 
comunicación de un sistema paralelo, así como la eficiencia de los 
mecanismos de comunicación entre procesadores. En el capítulo 7 
analizaremos nuevamente el problema de la coherencia de los datos, pero en 
los sistemas DSM: los directorios de coherencia. Dedicaremos el capítulo 8 a 
presentar las técnicas de paralelización de bucles y el reparto de tareas a los 
procesadores. Finalmente, en el capítulo 9 haremos un breve resumen de la 
situación actual de los sistema paralelos, analizando la lista top500, así como 
una breve presentación de las herramientas básicas para programar 
aplicaciones en paralelo: OpenMP, para los sistemas de memoria compartida 
SMP, y MPI, para el caso de paso de mensajes (en sistemas DSM o MPP). 
▪ 6 ▪ INTRODUCCIÓN 
 
 
 
 
▪ 1 ▪ 
 
 
Computadores Vectoriales 
 
 
 
 
 
 
 
 
 
1.1 ¿QUÉ ES UN COMPUTADOR VECTORIAL? 
Como hemos comentado en la introducción, las arquitecturas de tipo 
MIMD son las más adecuadas para resolver en paralelo aplicaciones de tipo 
general. Existen, sin embargo, algunos problemas importantes, desde el 
punto de vista del cálculo requerido, en los que es posible utilizar otro tipo 
de arquitecturas para lograr ejecuciones con un alto rendimiento. 
Como ya se sabe, en los programas de cálculo científico la mayor parte 
del tiempo de ejecución se invierte en la ejecución de bucles. Por ejemplo: 
 
do i = 0, N-1 
 C(i) = A(i) + B(i) 
enddo 
 
Si N es muy grande (N = 109, por ejemplo) el tiempo de ejecución de ese 
bucle será muy alto, a pesar de su estructura tan simple. Si lo ejecutamos en 
un procesador escalar, el código ensamblador será, por ejemplo, el siguiente: 
▪ 8 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
buc: FLD F1,A(R1) 
 FLD F2,B(R1) 
 FADD F3,F2,F1 
 FST C(R1),F3 
 
 ADDI R1,R1,#8 
 SUBI R2,R2,#1 
 BNZ R2,buc 
 
En un procesador escalar se ejecutaría, en el mejor de los casos, una 
instrucción por ciclo1, por lo que para ejecutar una iteración del bucle se 
necesitarían 7 ciclos; por tanto, el tiempo de ejecución de todo el programa 
sería de TE = 7N. 
El bucle anterior tiene dos características específicas. Por un lado, las 
estructuras de datos que utiliza —los vectores A, B y C— son muy regulares; 
y, por otro lado, todas las iteraciones del bucle se pueden ejecutar de manera 
independiente, ya que no existen dependencias de datos entre ellas. 
Para comenzar, definamos qué es, en este contexto, un vector. Un vector 
es una estructura que se puede definir mediante tres parámetros: 
 
• dirección de comienzo: dirección de memoria del primer elemento 
del vector. 
• longitud: número de elementos del vector. 
• paso (stride): distancia en memoria entre dos elementos consecutivos 
del vector. 
 
Por ejemplo, un vector que esté almacenado en las posiciones 1000, 1002, 
1004, 1006, 1008, 1010, 1012 y 1014 de memoria (cada componente ocupa 
una posición de memoria) se definiría así: 
 
 dirección de comienzo = 1000 longitud = 8 paso = 2 
 
Un procesador escalar, como su nombre indica, trabaja con escalares. Sin 
embargo, en las áreas de Ciencia e Ingeniería es muy común el uso de 
vectores y el tiempo de ejecución se invierte, principalmente, en la 
ejecución, una y otra vez, de bucles como el anterior. ¿Por qué no definir una 
arquitectura y un lenguaje máquina que directamente sean capaces de tratar 
con vectores? ¿Por qué no escribir el programa anterior de la siguiente 
manera? 
 
1 Si el procesador fuera superescalar, quizás se podría conseguir algo más de una instrucción por ciclo. 
1.1 ¿QUÉ ES UN COMPUTADOR VECTORIAL? ▪ 9 ▪ 
LV V1,A(R1) ; leer el vector A 
LV V2,B(R1) ; leer el vector B 
ADDV V3,V1,V2 ; sumar ambos vectores 
SV C(R1),V3 ; escribir el resultado en el vector C 
 
En este nuevo juego de instrucciones, la instrucción LV V1,A(R1) 
implicaría lo siguiente (utilizando, a modo de ejemplo, el esquema de 
segmentación que se muestra2): 
 
LV V1,A(R1) BD L AM M M M E 
 M M M E 
 M M M E 
 ... ... ... 
 M M M E 
 
Podríamos representar la ejecuciónanterior, de manera simplificada, de la 
siguiente forma: 
 
LV V1,A(R1) BD L AM M M M E E ... ... E 
 
Así pues, mediante una única instrucción leemos de memoria un vector 
completo de N elementos. Para que esto sea posible la memoria debe de estar 
segmentada, con lo que, si no existe algún otro impedimento, en cada ciclo 
proporcionará un elemento del vector, que se irán escribiendo en un registro 
vectorial. 
El siguiente esquema presenta la ejecución del programa anterior fase a 
fase (las latencias de las unidades funcionales son un simple ejemplo): 
 
LV V1,A(R1) BD L AM M M M E ... (N ciclos) ... 
LV V2,B(R1) BD L AM M M M E ... (N ciclos) ... 
ADDV V3,V1,V2 BD . . . . L A A E ... (N ciclos) ... 
SV C(R1),V3 BD L AM . . . . L M M M E ... ... E 
 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... ... 14+N 
 
 ti N 
 
(Por ahora, supongamos que los operandos que necesitan las instrucciones ADDV y SV se 
pueden obtener en los ciclos 8 y 11, tal como se indica en la tabla). 
 
2 Las fases de ejecución habituales: BD, búsqueda y descodificación de la instrucción; L, lectura de los 
operandos; AM, cálculo de la dirección de memoria; A, una operación en una unidad funcional; M, una 
operación en memoria; E, escritura del resultado en los registros. Cada instrucción utiliza solamente 
las fases que necesita para su ejecución. 
▪ 10 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
Si el modelo de ejecución es ese, podemos hacer un análisis sencillo para 
obtener el tiempo de ejecución del bucle (de manera simplificada; un poco 
más adelante formalizaremos este cálculo): existe un tiempo de inicio —ti— 
antes de que la última instrucción comience a escribir, y después, para 
terminar la ejecución, se necesitan N ciclos, uno por cada elemento del 
vector. Por tanto: 
 
TV = ti + N 
 
 
Si comparamos esta expresión con la que hemos obtenido para un 
procesador escalar, la mejora es clara. Por ejemplo, si el número de 
elementos de los vectores es N = 128, y si ti = 14 ciclos, tendríamos los 
siguientes tiempos de ejecución: 
 
TE = 7 N = 896 ciclos 
TV = ti + N = 142 ciclos (un 16%) 
 
No es ésta la única ventaja. Por un lado, han desaparecido las 
dependencias de control3 debida al bucle, ya que, por definición, ha 
desaparecido el propio bucle. Por otro lado, sólo se han ejecutado 4 
instrucciones, y no las 7N que componían el bucle escalar. Esto implica que 
el uso de la cache de instrucciones es mucho más bajo, y, por consiguiente, 
el tráfico en el bus también. 
Pero, por supuesto, todas esas ventajas no salen “gratis”. A decir verdad, 
tenemos que analizar con más detalle el esquema de ejecución anterior, para 
conocer los recursos que se necesitan para poder ejecutar de esa manera las 
instrucciones vectoriales. 
1.1.1 Algunos problemas 
1.1.1.1 La memoria de un computador vectorial 
Un procesador vectorial utiliza la memoria de modo intensivo. Por 
ejemplo, en el caso anterior tenemos 3 instrucciones, 2 LV y 1 SV, que están 
utilizando simultáneamente la memoria y, además, cada instrucción realiza N 
accesos a memoria. Por tanto, hay que solucionar dos aspectos: 
 
3 Las responsables de las dependencias de control son las instrucciones de salto. En general, después 
de la instrucción de dirección i se ejecuta la instrucción de dirección i+1, salvo en el caso de los 
saltos. Cuando ejecutamos un salto no sabemos qué instrucción será la siguiente hasta que el salto 
termine, por lo que hay que parar al procesador (aunque existen muchas técnicas para evitar esos 
ciclos "muertos"). 
1.1 ¿QUÉ ES UN COMPUTADOR VECTORIAL? ▪ 11 ▪ 
1. ¿Cuántos buses hay para acceder a memoria? El procesador y el 
sistema de memoria se comunican mediante el bus de datos. Una 
operación vectorial de memoria va a ocupar el bus de datos durante N 
ciclos (supongamos que se transfiere una palabra por ciclo). Por tanto, 
si sólo hubiera un bus, sólo una instrucción podría acceder a memoria 
en cada momento, y todas las demás deberían esperar a que ésta 
terminara. Por consiguiente, el tiempo de ejecución no sería de orden 
N, sino de kN (con k = 2, 3, 4..., número de instrucciones de memoria). 
 
2. ¿No habrá conflictos en el uso de los módulos de memoria? A pesar 
de que el espacio de memoria esté entrelazado entre los diferentes 
módulos de memoria, puede suceder que en un momento determinado 
se necesite acceder a elementos de vectores almacenados en el mismo 
módulo. Si sucede esto, para poder comenzar un acceso habrá que 
esperar a que termine el anterior acceso al mismo módulo, con lo que 
aumentará el tiempo de ejecución. 
 
Queda claro que el sistema de memoria de un computador vectorial juega 
un papel muy importante en el rendimiento final del sistema: hacen falta 
múltiples buses, y la memoria debe estar entrelazada en muchos módulos, 
para reducir los conflictos de acceso a los mismos. 
1.1.1.2 Unidades funcionales vectoriales 
Analizando el esquema de ejecución anterior, queda claro que las 
unidades funcionales deben estar segmentadas. Una única instrucción 
(ADDV, por ejemplo) realiza N operaciones en la unidad funcional, una por 
ciclo. Si no estuviera segmentada, no sería posible generar un dato por ciclo. 
De la misma manera, parece necesario poder disponer de varias unidades 
funcionales de cada tipo, ya que una instrucción ocupa cada unidad 
funcional durante N ciclos. 
1.1.1.3 Registros vectoriales 
¿Qué es un registro vectorial? ¿De qué tamaño son? ¿Cómo se leen y se 
escriben? En un registro vectorial se guardan los elementos de un vector. 
Cada elemento, normalmente, será un escalar representado en coma flotante, 
por ejemplo en 64 bits. Por tanto, en un registro tendremos 64 × N bits. El 
tamaño de los registros es, en todo caso, limitado. Es habitual que un registro 
vectorial permita almacenar 64 o 128 (Lmax) elementos de un vector, con lo 
▪ 12 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
que su capacidad sería de 64 (o 128) × 64 = 4 (u 8) kilobits. Si nos fijamos 
en el tamaño, se comprende fácilmente que no se disponga de un número 
muy elevado de registros vectoriales. Normalmente dispondremos de 8-16 
registros (16 × 8 = 128 kilobits). En algunas máquinas, el tamaño de los 
registros es variable; es decir, el "espacio de memoria" de que se dispone se 
puede utilizar para definir muchos registros de pocos elementos o unos 
pocos de muchos elementos. 
 
 
 
 
 
¿Qué se debe hacer cuando la longitud de los vectores que tenemos que 
procesar es mayor que Lmax (64 o 128 elementos)? No hay más remedio que 
formar un bucle, y en cada iteración del mismo procesar Lmax elementos 
(strip mining). Por tanto, aparecen de nuevo las dependencias de control, 
aunque esta vez cada 64 (128) elementos. 
En los primeros computadores vectoriales los registros se trataban como 
una “unidad”, por lo que no era posible leer y escribir sobre el mismo 
registro a la vez. Hoy en día, los elementos que conforman un registro 
vectorial se tratan como unidades independientes que pueden direccionarse 
de manera separada, con lo que es posible acceder a los primeros elementos 
de un vector ya almacenados en un registro mientras se sigue escribiendo el 
resto de elementos. Por otro lado, dado que diferentes instrucciones irán 
produciendo datos para escribir en el banco de registros vectoriales, y que 
cada una de ellas necesitará muchos ciclos para escribir el vector resultado, 
serán necesarios varios (muchos) buses de escritura (evidentemente, también 
se necesitan “muchos” buses de lectura). Con todo ello, el banco de registros 
de un procesador vectorial resulta ser un dispositivo complejo. 
1.1.1.4 Programas vectoriales 
¿Qué tipo de programas se pueden ejecutar en un computador vectorial? 
Los procesadores vectoriales están optimizados para procesar vectores, pero 
en los programas reales, además de procesar vectores, habrá que procesar 
código escalar.¿Cómo se hace eso? ¿Qué influencia tiene en la velocidad de 
cálculo? (como veremos, el efecto del código escalar puede ser muy grande). 
registros vectoriales U.F. 
1.1 ¿QUÉ ES UN COMPUTADOR VECTORIAL? ▪ 13 ▪ 
Analicemos de nuevo qué se hace cuando se procesan vectores. Veamos el 
siguiente ejemplo: 
 
do i = 0, N-1 
 A(i) = A(i) + 1 
enddo 
 
Si se ejecutara escalarmente, y simplificando, el orden de ejecución de las 
diferentes operaciones sería el siguiente (L = load; S = store; + = suma; i = 
elemento del vector): 
 
L0 +0 S0 / L1 +1 S1 / L2 +2 S2 / L3 +3 S3 / ... / LN–1 +N–1 SN–1 
 
Si lo ejecutáramos vectorialmente (LV - ADDV - SV), el orden pasaría a 
ser el siguiente: 
 
L0 L1 L2 ... LN–1 / +0 +1 +2 ... +N–1 / S0 S1 S2 ... SN–1 
 
Esto es, la ejecución vectorial implica desordenar el código original. Y 
como ya sabemos, esto no siempre es posible, ya que hay que respetar las 
dependencias de datos entre las instrucciones. Por tanto, para decidir si un 
programa se puede ejecutar vectorialmente o no, hay que hacer un 
meticuloso análisis de las dependencias, tarea que, como veremos, va a 
recaer, en gran medida, en un buen compilador vectorial. 
Resumiendo todo lo anterior: aunque hemos definido un modelo de 
computador con un rendimiento teórico muy elevado, en la realidad tenemos 
que superar muchos problemas para poder llegar a esa velocidad de cálculo. 
1.1.2 Arquitectura y lenguaje máquina 
Existen diferentes arquitecturas para los computadores vectoriales, casi 
tantas como fabricantes. En los primeros diseños, los computadores 
vectoriales no tenían registros, y todas las operaciones se hacían con los 
operandos en memoria. A este modelo se le denomina "Memoria-Memoria" 
(M/M). Pero pronto se añadieron los registros vectoriales; como 
consecuencia, los operandos de las operaciones vectoriales se obtienen de 
registros y los resultados se dejan en registros (modelo R/R). 
En la siguiente figura se muestra un esquema lógico, muy simple, de un 
computador vectorial. Podemos distinguir dos secciones: la sección escalar y 
la vectorial. El procesador escalar se encarga de la búsqueda y 
descodificación de las instrucciones. Si la instrucción es escalar, la ejecuta él 
▪ 14 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
mismo, utilizando los registros escalares necesarios; pero si es vectorial, 
pasa el control al procesador vectorial para que la ejecute. Salvo que 
especifiquemos alguna otra opción, vamos a suponer que la unidad de 
control es de tipo Tomasulo (desorden/desorden). 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Tal y como hemos comentado, aunque vamos a trabajar con vectores, en 
la realidad tendremos una mezcla de código vectorial y escalar. Por tanto, 
tendremos que utilizar tanto instrucciones vectoriales como escalares. Las 
instrucciones escalares son las habituales en cualquier procesador RISC. En 
función del computador, existen diferentes juegos de instrucciones 
vectoriales y de formatos de instrucciones; las más habituales son las 
siguientes (más tarde veremos algunas otras): 
 
OPV Vi,Vj,Vk Vi = Vj OP Vk 
 (OP = ADD, SUB, MUL, DIV...) 
 Operación entre dos vectores. El resultado es otro 
vector. 
 
OPVS Vi,Vj,Fk Vi = Vj OP Fk 
OPVI Vi,Vj,#inm Vi = Vj OP #inm 
 (OP = ADD, SUB, MUL, DIV...) 
 Operación entre un vector y un escalar. El 
resultado es un vector. 
 
 
Registros 
 
 
Unidades 
funcionales 
Procesador 
escalar 
(completo) 
Control del 
procesador 
vectorial 
Unidad de 
direcciones 
(datos) 
M
em
o
ri
a 
(op.) 
1.2 DEPENDENCIAS DE DATOS ▪ 15 ▪ 
LV Vi,A(Rj) Se lee a partir de la dirección de memoria A+Rj 
un vector, y se deja en el registro Vi (puede 
haber más modos de direccionamiento). 
 
SV A(Rj),Vi Similar a la anterior, pero, en lugar de leer, 
escribe un vector en memoria. 
 
Para identificar un vector en memoria, hay que dar tres parámetros: 
dirección de comienzo, longitud y paso. La dirección de comienzo se indica 
en la propia instrucción LV/SV (de acuerdo al modo de direccionamiento que 
se utilice). La longitud del vector y el paso, en cambio, hay que indicarlos 
previamente a la operación de lectura o escritura. Para ello utilizaremos dos 
registros especiales: VL (vector length), para indicar el número de elementos 
del vector, su longitud, y VS (vector stride), para indicar el paso. Si el 
contenido de VL es mayor que Lmax (tamaño de los registros vectoriales), 
sólo se procesarán Lmax elementos. 
Así pues, tenemos que ejecutar las siguientes instrucciones para, por 
ejemplo, leer un vector: 
 
 MOVI VL,#64 ; los vectores son de 64 elementos 
 MOVI VS,#8 ; el paso es 8 
 LV V1,A(R1) 
 
De esta manera se cargarán en el registro V1 64 elementos de un vector, 
correspondientes a las direcciones A+R1, A+R1+8, A+R1+16… 
En algunos computadores es necesario indicar explícitamente el paso de 
los vectores en la propia instrucción, utilizando para ello un registro de 
propósito general. 
 
1.2 DEPENDENCIAS DE DATOS 
Al igual que sucede con los procesadores (super)escalares, la velocidad de 
cálculo de los procesadores vectoriales está limitada por las dependencias de 
datos. Una instrucción depende de otra anterior si uno de sus operandos es el 
resultado de dicha instrucción, por lo que deberá esperar a que finalice antes 
de poder ejecutarse. Ya sabemos que, en los procesadores escalares, para 
▪ 16 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
atenuar la pérdida de rendimiento debida a las dependencias de datos se 
utilizan cortocircuitos (forwarding) entre las unidades funcionales; una idea 
similar se aplica también en los procesadores vectoriales. 
Para los siguientes ejemplos utilizaremos el siguiente esquema de 
segmentación (Tomasulo): 
 
 LV/SV → BD L AM M M M E 
 ADDV → BD L A A E 
1.2.1 Encadenamiento (chaining) 
Se dice que dos instrucciones se encadenan si la segunda utiliza el vector 
generado por la primera sin esperar a que ésta lo haya guardado en el 
registro vectorial. Veamos un ejemplo sencillo: 
 
do i = 0, N-1 LV V1,A(R1) 
 A(i) = A(i) + 1 → ADDVI V2,V1,#1 
enddo SV A(R1),V2 
 
El bucle presenta dependencias de datos muy claras: LV → ADDVI (V1) y 
ADDVI → SV (V2). Entonces ¿cómo se ejecutará ese programa? Tenemos 
dos alternativas: sin realizar encadenamiento entre las dos instrucciones, o 
encadenándolas. 
 
a. Si no se realiza encadenamiento, la segunda instrucción deberá esperar 
a que termine la primera, para poder leer el registro vectorial 
correspondiente (V1). En la figura se muestra un esquema de 
ejecución, en el que se puede ver cuándo se realizan las lecturas (L). 
 
LV V1,A(R1) BD L AM M M M E ... E 
ADDVI V2,V1,#1 BD . . . . . ... . L A A E ... E 
SV A(R1),V2 BD L AM . . ... . . . . . ... . L M M M E ... 
ciclos ← 6 → ← N → ← 3 → ← N → ← 4 → ← N 
 
 Por tanto, el tiempo de ejecución en este caso es TV = 13 + 3N ciclos. 
 
b. En cambio, si se realiza encadenamiento, según se van generando los 
vectores se van utilizando en la siguiente unidad funcional; es decir, se 
utiliza el cortocircuito E → L. 
1.2 DEPENDENCIAS DE DATOS ▪ 17 ▪ 
LV V1,A(R1) BD L AM M M M E E ... (N cicl.) ... 
ADDVI V2,V1,#1 BD . . . . L A A E E ... (N cicl.) ... 
SV A(R1),V2 BD L AM . . . . L M M M E ... ... E 
ciclos ← 6 → ← 3 → ← 4 → ← N → 
 
 En este segundo caso, el tiempo de ejecución es TV = 13 + N ciclos. 
 
Podemos analizar el mismo comportamiento de manera cualitativa. Por 
ejemplo, la siguiente figura muestra un esquema de ejecución muy 
simplificado del programa anterior (LV / ADDVI / SV), en función de si se 
encadenan o no las instrucciones: 
 
 
 
 
 sin encadenamiento: T ~ 3N con encadenamiento: T ~ N 
 
La diferencia entre ambas opciones es clara. En el primer caso, el tiempo 
de ejecución es del orden de 3N; en el segundo, en cambio, es deorden N. 
Por ejemplo, para N = 64 el tiempo de ejecución bajaría de 13 + 3×64 = 205 
ciclos a 13 + 64 = 77 ciclos (un 38%). Así pues, necesitamos poder 
encadenar las instrucciones para conseguir un buen rendimiento. 
 
1.2.1.1 Encadenamiento con dos instrucciones 
En el ejemplo del apartado anterior, el encadenamiento se ha realizado 
con una única instrucción anterior: la instrucción ADDVI con la instrucción 
LV, o la instrucción SV con la instrucción ADDVI. En un caso más general, 
tendríamos que poder encadenar una instrucción con dos instrucciones 
anteriores. Veamos un ejemplo (C = A + B): 
 
LV V1,A(R1) BD L AM M M M E E ... (N ciclos) ... 
LV V2,B(R1) BD L AM M M M E E ... (N ciclos) ... 
ADDV V3,V1,V2 BD . . . . L A A E E ... (N ciclos) ... 
SV C(R1),V3 BD L AM . . . L M M M E ... ... E 
 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 14+N 
LV 
ADDVI 
SV 
ADDVI 
SV 
LV 
▪ 18 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
La tercera instrucción (ADDV) necesita los vectores V1 y V2, que son 
generados por las dos primeras instrucciones respectivamente. Pero estos dos 
vectores no se generan sincronizados: el primero se comienza a generar en el 
ciclo 7 y el segundo en el 8 (y a partir de ahí el resto de elementos). Por 
tanto, en el ciclo 7 no está preparado el primer elemento del segundo 
operando (V2), y en el ciclo 8 se pierde la posibilidad de tomar el primer 
elemento del primer operando (V1) (los datos no se “pierden”, claro ya que 
se están cargando en el registro vectorial). ¿Qué se puede hacer? 
Para poder efectuar el encadenamiento hay que coger un operando según 
sale de la unidad funcional y leer el otro del registro correspondiente (V1), 
donde ya se está escribiendo. Para ello es necesario que el banco de registros 
permita la lectura y escritura simultánea del mismo registro (lo habitual en 
las máquinas vectoriales actuales, y que se conoce como flexible chaining o 
encadenamiento flexible); si eso no es posible, se perderá la posibilidad de 
encadenar (salvo que se aplique alguna otra solución) y habrá que esperar a 
que finalice la escritura de ambos operandos. 
1.2.2 Tablas de ejecución de las instrucciones 
Representar los esquemas de ejecución de un conjunto de instrucciones 
vectoriales fase a fase es un poco pesado. Por ello, en lugar de hacer ese tipo 
de esquemas, vamos a resumir en una tabla las acciones principales que 
suceden cuando se ejecutan las instrucciones: 
 
• Inicio de ejecución: cuántos ciclos han pasado, desde el comienzo, 
hasta el momento previo a iniciar la operación en la UF. El inicio 
puede ser tras la lectura de los registros, o mediante encadenamiento, 
en cuyo caso indicaremos el número de ciclos entre [ ]. 
 (La ejecución de instrucciones es segmentada, y las instrucciones se ejecutan de una 
en una, no es superescalar.) 
• Latencia de la unidad funcional. 
• Ciclo en el que se genera el primer elemento. 
• Ciclo en el que se genera el último (N) elemento. 
 
Por ejemplo, para una instrucción LV la tabla correspondiente sería: 
 
BD L AM M M M E ... ... E 
comienzo (3) lat. UF (3) dato 1 (6+1) dato N (6+N) 
1.2 DEPENDENCIAS DE DATOS ▪ 19 ▪ 
Las ejecuciones de los dos ejemplos anteriores se pueden resumir así: 
 
 sin encadenamiento con encadenamiento 
A = A + 1 inic. lat. UF dato 1 dato N inic. lat. UF dato 1 dato N 
LV V1,A(R1) 3 3 6+1 6+N 3 3 6+1 6+N 
ADDVI V2,V1,#1 6+N+1 2 9+N+1 9+2N [7] 2 9+1 9+N 
SV A(R1),V2 9+2N+1 3 13+2N+1 13+3N [10] 3 13+1 13+N 
 
Si la ejecución de las instrucciones no se encadena, la instrucción ADDVI 
tiene que esperar a que termine la escritura en el registro V1 (ciclo 6+N) y 
luego leer del registro (+1). Lo mismo le sucede a la instrucción SV: tiene 
que esperar a que la instrucción ADDVI termine (9+2N), y entonces leer V2 
y escribir en memoria. 
Si la ejecución de las instrucciones se encadena, la suma puede comenzar 
en el ciclo 7 (en ese ciclo llega de memoria el primer elemento del vector), y 
la escritura en memoria puede comenzar en el ciclo 10 (ciclo en que la suma 
genera el primer dato). 
 
En el segundo ejemplo podemos observar el mismo comportamiento. Si 
no se puede encadenar, la instrucción ADDV tiene que esperar a tener listos 
ambos operandos (ciclo 7+N) y entonces leerlos. Cuando se encadena, uno 
de los operandos (V2) se obtiene directamente de la memoria y el otro (V1) 
del registro (donde se ha escrito en el ciclo anterior); el ciclo de 
encadenamiento es, por tanto, el ciclo 8. 
 
 sin encadenamiento con encadenamiento 
C = A + B inic. lat. UF dato 1 dato N inic. lat. UF dato 1 dato N 
LV V1,A(R1) 3 3 6+1 6+N 3 3 6+1 6+N 
LV V2,B(R1) 4 3 7+1 7+N 4 3 7+1 7+N 
ADDV V3,V1,V2 7+N+1 2 10+N+1 10+2N [8] 2 10+1 10+N 
SV C(R1),V3 10+2N+1 3 14+2N+1 14+3N [11] 3 14+1 14+N 
 
Nota: estamos aplicando un modelo “didáctico” de ejecución vectorial, y el objetivo 
es mostrar el comportamiento general, no los detalles particulares. Lo computadores 
comerciales utilizan estrategias similares, aunque los detalles de implementación 
pueden variar. 
▪ 20 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
1.3 DEPENDENCIAS ESTRUCTURALES 
Después de analizar las dependencias de datos, analicemos las 
dependencias estructurales. Recuerda que un conflicto o dependencia 
estructural surge cuando se quiere utilizar un recurso mientras está ocupado 
por otra instrucción. Además de las unidades funcionales, el recurso más 
importante en un computador vectorial es la memoria. Para poder utilizar la 
memoria, primeramente hay que disponer de un bus libre. ¿Cuántos buses 
tenemos para acceder a la memoria? Por otro lado, se utilizan los propios 
módulos de memoria. ¿Están libres los módulos que hay que utilizar? Si 
están ocupados, ¿cuánto tiempo hay que esperar? 
1.3.1 Buses de memoria (unidades funcionales LV/SV) 
La ejecución de las instrucciones LV y SV implica una transferencia con 
memoria en la que se utilizan los buses. Cuando se ejecuta una instrucción 
LV o SV, el bus se ocupa durante N ciclos; mientras una instrucción está 
utilizando el bus, la siguiente deberá esperar hasta que se libere el bus. Por 
tanto, si el computador no tuviera un número suficiente de buses, la 
velocidad de cálculo de la máquina no sería muy alta. 
Analicemos la influencia del número de buses mediante el ejemplo 
anterior (A = A + 1; LV / ADDVI / SV). Supongamos que la máquina puede 
encadenar las instrucciones, pero que sólo posee un bus para trabajar con 
memoria (LV o SV)4. En estas condiciones, cuando la instrucción SV quiere 
empezar a escribir en memoria, en el ciclo de encadenamiento, el bus no está 
disponible, ya que lo ocupa la instrucción LV (y lo mantendrá ocupado 
muchos ciclos). Por tanto, deberá esperar hasta que termine la primera 
instrucción (y se libere el bus) y leer entonces el registro en el que se están 
escribiendo los resultados (V2)5. 
Este sería el esquema de ejecución: 
 
 
 
4 Los buses de memoria pueden usarse tanto para una lectura como para una escritura; en algunas 
máquinas, en cambio, los buses están "dedicados": unos son sólo para leer y otros sólo para escribir. 
5 Si no puede leerse un registro mientras se está escribiendo, entonces habrá que esperar a que finalice 
la escritura. 
1.3 DEPENDENCIAS ESTRUCTURALES ▪ 21 ▪ 
LV V1,A(R1) BD L AM M M M E E ... (N ciclos) ... E 
ADDVI V2,V1,#1 BD . . . . L A A E E ... (N ciclos) ... E 
SV A(R1),V2 BD L AM . . . . ? . ... ... L M M M E ... (N cicl.) 
 bus ocupado... libre 
 
o, esquemáticamente: 
 
 
 
 
La tabla correspondiente a la ejecución sería la siguiente: 
 
 un bus / encadenamiento 
A = A + 1 inic. lat. UF dato 1 dato N 
LV V1,A(R1) 3 3 6+1 6+N 
ADDVI V2,V1,#1 [7] 2 9+1 9+N 
SV A(R1),V2 [6+N] 3 9+N+1 9+2N 
 
Repitamos el análisis, pero con el segundo ejemplo que hemos visto antes. 
En ambos casos, las instrucciones se encadenan, pero en el primer caso la 
máquinacuenta con un solo bus de memoria, y en el otro caso cuenta con 
dos buses. 
 
 un bus / encadenamiento dos buses / encadenamiento 
C = A + B inic. lat. UF dato 1 dato N inic. lat. UF dato 1 dato N 
LV V1,A(R1) 3 3 6+1 6+N 3 3 6+1 6+N 
LV V2,B(R1) 6+N 3 9+N+1 9+2N 4 3 7+1 7+N 
ADDV V3,V1,V2 [10+N] 2 12+N+1 12+2N [8] 2 10+1 10+N 
SV C(R1),V3 [9+2N] 3 12+2N+1 12+3N [6+N] 3 9+N+1 9+2N 
 
Cuando sólo hay un bus, la segunda instrucción LV no puede utilizar la 
memoria hasta que el primer LV la deje de utilizar, y lo mismo le sucede a la 
instrucción SV (para cuando se libera el bus, la escritura en el registro V3 
está terminando). Por tanto, el tiempo de ejecución es de orden 3N. Si la 
ADDVI 
SV 
LV 
T ~ 2N 
▪ 22 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
máquina tiene dos buses, las instrucciones LV se ejecutarán a la vez, pero la 
instrucción SV tendrá que esperar. 
Esquemáticamente: 
 
 
 
 
 un bus / encadenamiento: 3N dos buses / encadenamiento: 2N 
 
La conclusión es sencilla: si no existen suficientes recursos (buses) para 
poder ejecutar las instrucciones de memoria, a pesar de tener la posibilidad 
de encadenar las instrucciones el tiempo de ejecución será elevado. 
 
En resumen, los resultados que hemos obtenido con ambos ejemplos son 
los siguientes: 
 
1. A = A + 1 (N = 64) 
sin encadenamiento 13 + 3N = 205 ciclos → 3,20 ciclos/dato 
encadenamiento / 1 bus 9 + 2N = 137 ciclos → 2,14 c/d 
encadenamiento / 2+ buses 13 + N = 77 ciclos → 1,20 c/d 
 
2. C = A + B (N = 64) 
sin encadenamiento / 1 bus 16 + 4N = 272 ciclos → 4,25 c/d 
sin encadenamiento / 3 buses 14 + 3N = 206 ciclos → 3,22 c/d 
encadenamiento / 1 bus 12 + 3N = 204 ciclos → 3,19 c/d 
encadenamiento / 2 buses 9 + 2N = 137 ciclos → 2,14 c/d 
encadenamiento / 3 buses 14 + N = 78 ciclos → 1,22 c/d 
 
Los datos muestran claramente la importancia de disponer de suficientes 
buses a memoria y de que las instrucciones puedan encadenarse para que las 
instrucciones se ejecuten eficientemente. 
1.3.2 Conflictos en el uso de los módulos de memoria 
1.3.2.1 Una sola operación de memoria 
Tras haber analizado el problema de los buses en un procesador vectorial, 
analicemos ahora el uso de la propia memoria. La memoria de cualquier 
computador está entrelazada en varios módulos; así, las direcciones i e i+1 
LV 
LV 
SV 
ADDV 
LV 
LV 
SV 
ADDV 
1.3 DEPENDENCIAS ESTRUCTURALES ▪ 23 ▪ 
no corresponden al mismo módulo de memoria, sino a módulos 
consecutivos. De esta manera es posible, por ejemplo, efectuar una 
operación simultánea en dos (en general nm, el número de módulos) 
direcciones consecutivas; si estuvieran en el mismo módulo tendríamos que 
esperar a que finalizara una operación antes de empezar con la siguiente. 
Cuando se ejecuta una instrucción LV o SV se efectúan N lecturas o 
escrituras en memoria, una por ciclo. Para que se haga de manera eficiente, 
es necesario que se acceda a módulos que estén libres; si no, tendríamos un 
conflicto estructural, y no lograríamos efectuar una operación por ciclo. 
Veamos el problema con un ejemplo. Hay que leer el vector A(A0:A15); la 
memoria está entrelazada en 4 módulos, y el vector se encuentra en módulos 
consecutivos (s = 1) a partir de m0. La latencia de la memoria es de 3 ciclos. 
La situación de la memoria según se lee el vector A es la siguiente: 
 
m0 m1 m2 m3 
A0 A1 A2 A3 
A4 A5 A6 A7 
A8 A9 A10 A11 
A12 A13 ... 
 
→ tiempo (ciclos) 
m0 M M M M M M ... 
m1 M M M M M M ... 
m2 M M M M M M 
m3 M M M M M M 
 
La lectura comienza en m0, y sigue en m1, m2, m3, y se vuelve a m0, 
para seguir leyendo más elementos del vector. En ese momento, m0 está 
libre, puesto que ya ha terminado el primer acceso, y por tanto no tendremos 
ningún problema. 
Pero si, por ejemplo, la latencia de la memoria fuera de 8 ciclos, al ir a 
utilizar nuevamente m0 lo encontraríamos ocupado, ejecutando todavía la 
operación anterior. Tendríamos, por tanto, que esperar a que finalizara antes 
de poder seguir leyendo el vector. Como consecuencia del conflicto 
estructural, el tiempo de ejecución de la operación sería más alto. 
El problema puede ser grave, en función de la definición del vector. Por 
ejemplo, si el paso del vector del ejemplo anterior fuera s = 4, entonces todos 
los elementos del vector estarían en el mismo módulo, m0: todos los accesos 
significarían un conflicto, ya que cada acceso dura 3 ciclos. 
▪ 24 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
Para analizar si surgirán o no conflictos en memoria, hay que considerar 
tres parámetros: el tiempo de acceso o latencia de la memoria —tm—, el 
número de módulos en que está entrelazada la memoria —nm—, y el paso 
de los vectores —s—. Dos de esos parámetros, latencia y número de 
módulos, son decisiones de diseño: se deciden al construir la máquina y no 
son modificables por el usuario. El tercero, en cambio, el paso de los 
vectores, corresponde al programa concreto que se ejecuta, y puede 
modificarse para intentar evitar conflictos. 
Cuando s = 1, se utilizan todos los módulos de memoria al acceder a un 
vector (m0-m1-m2-...). Por tanto, para que no haya conflictos se debe 
cumplir que: 
 
mtnm ≥ 
De esa manera, cuando hay que reutilizar un determinado módulo ya han 
pasado al menos nm ciclos, y por tanto estará libre. 
Para el caso general, s > 1, hay que calcular cuántos módulos se utilizan 
en una determinada operación. Por ejemplo, en el caso anterior, cuando s = 
4, sólo se utiliza un módulo de memoria, siempre el mismo (m0). Puede 
demostrarse fácilmente que el número de módulos que se utilizan en una 
operación de memoria es: 
),( snmMCD
nm
 (MCD = máximo común divisor) 
Así pues, y generalizado el resultado anterior, no habrá conflictos en 
memoria si el número de módulos que se van a utilizar es mayor o igual que 
la latencia: 
mtsnmMCD
nm
≥
),( 
 
Analizando la expresión anterior. se observa que la mejor situación se 
corresponde con el caso MCD(nm,s) = 1, es decir cuando, nm y s son primos 
entre sí, ya que se utilizan todos los módulos de memoria. En los casos más 
habituales, nm es una potencia de 2 (8, 16, 32, 64...). En esos casos, y si no 
hay conflicto cuando s = 1, no habrá conflictos para cualquier vector de paso 
impar (1, 3, 5...), pero podría haberlos para los casos de s par. 
Existe una situación óptima. Si el número de módulos de memoria, nm, es 
un número primo, entonces cualquier paso s será primo con él (salvo sus 
múltiplos). Por ejemplo, si nm = 5, no hay problemas con los vectores de 
1.3 DEPENDENCIAS ESTRUCTURALES ▪ 25 ▪ 
paso s = 1, 2, 3, 4, 6, 7, 8... Por desgracia, cuando la memoria se entrelaza en 
un número primo de módulos, 17 por ejemplo, calcular el módulo y la 
dirección dentro del módulo que corresponden a una palabra dada es una 
operación “compleja” (operación que debe realizar el controlador de 
memoria, para efectuar cualquier acceso a memoria), ya que habrá que 
efectuar una división para obtener el cociente y el resto (cuando nm = 2i, los 
i bits de menos peso indican el módulo, y el resto la dirección dentro del 
módulo). Debido a esa división, no se suele entrelazar la memoria en un 
número primo de módulos. 
El valor de s es muy variable en los programas vectoriales. Por ejemplo, al 
procesar matrices pueden definirse diferentes tipos de vectores: filas, 
columnas, diagonales... Para multiplicar dos matrices, por ejemplo, hay que 
usar filas en una y columnas en la otra. En algunos casos, para optimizar el 
acceso a memoria, las matrices no se guardan en posiciones consecutivas de 
memoria, sino que se dejan huecos sin ocupar (padding). 
Veamos un ejemplo de la utilidad de esta estrategia. Sea una memoria 
entrelazada en 4 módulos y una matriz de tamaño 4×4, de la que se van a 
utilizar las filas y las columnas. Como puede verse, no hay problemas en el 
acceso a una fila (s = 1), ya que los elementos están en módulos de memoria 
diferentes, pero el acceso a cualquier columna es muy conflictivo,ya que 
todos los elementos están en el mismo módulo de memoria. Sin embargo, si 
se dejan huecos en memoria entre los elementos de la matriz (en la tabla se 
muestra un ejemplo), entonces es posible acceder tanto a filas (s = 1) como a 
columnas (ahora s = 5) sin conflictos (aunque se generen conflictos en el 
acceso a las diagonales6). 
 
m0 m1 m2 m3 m0 m1 m2 m3 
A00 A01 A02 A03 A00 A01 A02 A03 
A10 A11 A12 A13 → - A10 A11 A12 
A20 A21 A22 A23 A13 - A20 A21 
A30 A31 A32 A33 A22 A23 - A30 
 A31 A32 A33 - 
 
sf = 1 sin conflictos sf = 1 sin conflictos 
sc = 4 conflictos (todos en m0) sc = 5 sin conflictos 
sD = 5 sin conflictos sD = 6 conflictos 
sd = 3 sin conflictos sd = 4 conflictos 
 
6 Como hemos comentado, el ideal sería que nm fuera un número primo. Por ejemplo, si nm fuera 5, 
los cuatro vectores del ejemplo (f, c, D y d) podrían accederse sin problemas si se dejan los 
correspondientes huecos (lo dejamos como ejercicio para el lector). 
▪ 26 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
1.3.2.2 Varias operaciones de memoria 
Como acabamos de ver, la ejecución de una instrucción vectorial de 
memoria, LV o SV, puede producir problemas en el acceso a los módulos de 
memoria. Lo mismo ocurre cuando se están ejecutando más de una 
instrucción de memoria. Aunque cada una de ellas no tuviera conflictos 
consigo misma, es posible que existan colisiones entre ellas; es decir, que 
una segunda instrucción quisiera utilizar un módulo de memoria ocupado en 
ese instante por otra instrucción. 
Analicemos el problema mediante un ejemplo. Supongamos que la 
memoria está entrelazada en 8 módulos y que la latencia es 3 ciclos (con el 
mismo esquema de segmentación de los ejemplos anteriores). Hay 
suficientes buses a memoria y las instrucciones pueden encadenarse. El 
primer elemento de A esta en el módulo m0 y el paso es s = 1. 
 
 2 buses / encadenamiento 
A = A + 1 inic. lat. UF dato 1 dato N 
LV V1,A(R1) 3 3 6+1 6+N 
ADDVI V2,V1,#1 [7] 2 9+1 9+N 
SV A(R1),V2 [10] 
 
Como hemos visto antes, la instrucción SV puede encadenarse en el ciclo 
10, e ir a memoria. Pero, ¿cómo se encuentran en ese momento los módulos 
de memoria, libres u ocupados? El esquema siguiente muestra el uso de los 
módulos de memoria ciclo a ciclo. La instrucción LV comienza la lectura en 
el ciclo 4, en el módulo m0, por ejemplo. Supongamos, por simplificar el 
problema, que el paso de A es 1. Por tanto, tras el módulo m0 se accederá a 
m1, m2..., m7, y nuevamente a m0, m1... Dado que la latencia es 3 ciclos, la 
instrucción no tiene ningún conflicto consigo misma (nm ≥ tm). 
 
 t (ciclos) 
mem. 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
m0 M M M - M M M m m m 
m1 M M M - - M M M m m m 
m2 M M M M M M 
m3 M M M M M M 
m4 M M M M M M 
m5 M M M M M M 
m6 M M M M M 
m7 M M M M 
1.3 DEPENDENCIAS ESTRUCTURALES ▪ 27 ▪ 
La instrucción SV puede encadenarse en el ciclo 10, para empezar en 
memoria en el ciclo 11, en el módulo m0 (hay que guardar el mismo vector, 
A). ¿Cómo se encuentra en ese momento ese módulo? La instrucción LV está 
en ejecución, y mantiene ocupados varios módulos. Primeramente debemos 
calcular qué módulo va a acceder LV en ese ciclo, para lo que basta con 
saber cuántos ciclos lleva ya en memoria y de qué módulo partió. En este 
ejemplo, la distancia en ciclos es de 10 – 3 = 7, y dado que partió del módulo 
m0 (y que s = 1), en el ciclo 11 irá a utilizar el módulo m7. Ese módulo, por 
tanto, no estará disponible. 
Pero habrá más módulos ocupados, ya que todavía estarán ejecutándose 
accesos que comenzaron en ciclos anteriores. Si la latencia de la memoria es 
tm, se mantiene ocupados tm–1 módulos anteriores. En el ejemplo tm = 3, 
por lo que tendremos dos módulos ocupados, m6 y m5. Utilizando el mismo 
razonamiento, es necesario dejar libres por delante otros tm–1 módulos, para 
que la instrucción LV pueda seguir ejecutándose sin interferencias; en el 
ejemplo, los módulos m0 y m1 (si no, LV “chocaría” con SV en el siguiente 
ciclo). 
Así pues, en este ejemplo en el ciclo 11 están ocupados o reservados los 
módulos <m5 – m6 – m7 – m0 – m1>. Si alguna instrucción quiere utilizar 
esos módulos, deberá esperar a que se liberen. Ése es el caso de la 
instrucción SV, que tiene que utilizar m0, y que tendrá que esperar. 
¿Cuántos ciclos? Tantos como la posición que ocupa el módulo a utilizar en 
la lista de módulos ocupados. En el ejemplo, 4 ciclos. En el ciclo 11 están 
ocupados los módulos m5-...-m1; en el siguiente ciclo, por tanto, los 
módulos m6-...-m2; en el siguiente, m7-...-m3, y, en el siguiente, m0-...-m4. 
Finalmente, en el siguiente ciclo se liberará m0, que podrá ser utilizado por 
la instrucción SV para comenzar la escritura del vector A. 
En resumen, para analizar los conflictos en memoria entre las 
instrucciones j y k, el procedimiento es el siguiente (todas las operaciones son 
módulo nm, siendo nm el número de módulos de memoria): 
 
a. Se calcula qué módulo va a comenzar a utilizar la instrucción j cuando 
la instrucción k quiere acceder a memoria: 
 
 (inik – inij) + módulo_inij (ini = ciclo inicio en memoria) 
 
b. Se crea la lista de módulos ocupados, añadiendo tm–1 módulos por 
delante y por detrás al módulo anterior (tm, latencia de la memoria). 
 
 < tm–1 módulos | (inik – inij) + módulo_inij | tm–1 módulos> 
▪ 28 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
c. Si el primer módulo de memoria que va a utilizar la instrucción k está 
ocupado, se calcula el tiempo de espera, que habrá que añadir al 
tiempo de inicio de la instrucción (antes de la UF). 
 
El procedimiento anterior se puede generalizar para el caso de acceso a vectores con 
paso s, siempre que los pasos de las instrucciones que están accediendo a memoria 
sean iguales. En este caso, dado que en cada paso se avanzan s módulos, por un 
lado, habrá que hacer (inik – inij) × s + módulo_inij y luego habrá que contar tm–1 
módulos de s en s. En caso de que no coincida el paso de todas las instrucciones a 
memoria el análisis es más complejo y, normalmente, en función de la máquina, no 
se comenzará a ejecutar la segunda instrucción hasta que haya terminado la primera. 
 
Repitamos el ejercicio anterior, pero teniendo en cuenta los conflictos en 
memoria. Tal y como se muestra en la tabla, la instrucción SV quiere realizar 
un encadenamiento en el ciclo 10. Dado que la instrucción LV está aún en 
memoria, no podrá utilizar los siguientes módulos: el módulo (10 – 3) + 0 = 
7, los dos anteriores, 6 y 5, y los dos siguientes, 0 y 1. Como necesita 
acceder al módulo 0, el tiempo de espera será de 4 ciclos. Tras ese tiempo, 
ciclo 14, el encadenamiento no se podrá realizar directamente del sumador, 
sino que habrá que realizarlo desde el registro (si esto no fuera posible, 
habría que esperar a que la suma terminara de escribir en el registro V2). 
 
 2 buses / encadenamiento 
A = A + 1 inic. mod. ocup. t. esp. lat. UF dato 1 dato N 
LV V1,A(R1) 3 - - 3 6+1 6+N 
ADDVI V2,V1,#1 [7] - - 2 9+1 9+N 
SV A(R1),V2 [10] ?? 5 / 6 –7– 0 / 1 +4 3 17+1 17+N 
 
En general, para calcular el número de ciclos que hay que esperar para 
utilizar la memoria, hay que hacer el análisis con todas las instrucciones que 
estén en memoria, ya que cada una de ellas ocupará tm módulos de memoria. 
Como consecuencia de ello, no se pueden permitir más de nm div tm 
operaciones de memoria simultáneamente, ya que con ese número de 
instrucciones se ocupan todos los módulos de memoria. Por ejemplo, para el 
anterior caso (nm = 8 y tm = 3) no se pueden procesar simultáneamente más 
de 8 div 3 = 2; estaría de sobra, por tanto, un hipotético tercer bus a 
memoria. 
1.3 DEPENDENCIAS ESTRUCTURALES ▪ 29 ▪ 
De los párrafos anteriores se pueden deducir dos consecuencias 
importantes: por un lado, se necesita que el nivel de entrelazado del sistema 
de memoria sea grande, para que sepueda mantener el flujo de datos sin 
conflicto; y, por otro, va a ser importante una correcta colocación de los 
vectores en memoria para evitar colisiones en el acceso a diferentes vectores, 
para lo que puede ser importante el papel que juegue el compilador. 
1.3.3 Longitud de los registros vectoriales (strip mining) 
Otro factor que limita el rendimiento de la ejecución vectorial es el 
tamaño de los registros vectoriales. Los registros vectoriales se utilizan con 
el mismo propósito que los escalares: mantener cerca del procesador los 
datos que se van a utilizar. Los registros vectoriales son un recurso limitado. 
Por una parte, se trata de un número de registros no muy alto, menor en todo 
caso que el de registros escalares. Por otra parte, el número de buses de 
escritura y lectura también será limitado, y además se mantienen ocupados a 
lo largo de muchos ciclos (con la instrucción ADDV V3,V2,V1 se ocupan 
tres buses durante N ciclos). Por ello, necesitaremos bastantes buses para 
poder efectuar simultáneamente más de una operación con registros. 
Una tercera limitación proviene del tamaño de los registros vectoriales. Si 
bien los vectores que procesa el usuario pueden ser de cualquier tamaño, los 
registros vectoriales suelen ser de un tamaño limitado, habitualmente de 64-
128 palabras. Por tanto, con una instrucción vectorial no se pueden procesar 
más elementos que los correspondientes al tamaño de los registros. Para 
vectores más largos hay que montar un bucle y procesar el vector en trozos 
de tamaño Lmax. A este procedimiento se le denomina strip mining. 
El tamaño de los vectores que hay que leer o escribir en memoria se indica 
en un registro especial, VL (vector length). Si VL ≤ Lmax, se procesará el 
número de elementos indicado en VL; por el contrario, si VL > Lmax, entonces 
se procesarán únicamente Lmax elementos. Por ejemplo: 
 
 
 
do i = 0, N-1 
 A(i) = A(i) + 1 → 
enddo 
 
 MOVI VS,#1 
 MOVI R1,#N 
mas: MOV VL,R1 
 
 LV V1,A(R2) 
 ADDVI V2,V1,#1 
 SV A(R2),V2 
 
 ADDI R2,R2,#Lmax (× tam. pal.) 
 SUBI R1,R1,#Lmax 
 BGTZ R1,mas 
▪ 30 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
Según la longitud de los vectores, puede ser que el último (o el primer) 
trozo que se procese sea más pequeño que el resto. Por ejemplo, si tenemos 
Lmax = 128 y N = 1.000, en la última iteración sólo se procesarán 104 
elementos (7 × 128 + 104 = 1.000). 
Todo ello va a repercutir en el tiempo de ejecución de la operación 
vectorial (en la velocidad de cálculo, por tanto). Si el tiempo de una 
operación vectorial puede darse como TV = ti + tv N, el hecho de que los 
registros sean de tamaño Lmax hará que el tiempo de ejecución se exprese 
como: 
 
Nttt
L
NT vbuciV ++





= )(
max
 
 
N/Lmax indica el número de trozos a procesar (iteraciones del bucle), y tbuc 
el tiempo necesario para el control del bucle (ahora el sumando inicial 
también depende de N). 
El proceso de strip mining es prácticamente inevitable cuando hay que 
procesar vectores, ya que, en la mayoría de los casos, la longitud de los 
vectores es un parámetro que se decide en ejecución. Por tanto, incluso en el 
caso de que cupiera el vector en el registro, habrá que “pagar” una vez el 
coste del control del bucle, tbuc. 
Veamos un ejemplo: 
 
 N = 500 TV = 30 + 3N → TV = 30 + 1.500 = 1.530 ciclos (idea) 
 3,06 ciclos/elemento 
pero 
< 
 Lmax = 64 tbuc = 10 ciclos → TV = 8 × (30+10) + 1.500 = 1.820 ciclos 
 3,64 ciclos/elemento (+ 19%) 
 
1.4 VELOCIDAD DE CÁLCULO DE LOS COMPUTA-
DORES VECTORIALES 
Cuando hemos definido los computadores vectoriales hemos indicado el 
deseo de construir una máquina de gran velocidad en la ejecución de 
determinados programas. Analicemos un poco más despacio los parámetros 
básicos que definen la velocidad de cálculo en estas máquinas. 
1.4 VELOCIDAD DE CÁLCULO DE LOS COMPUTA-DORES VECTORIALES ▪ 31 ▪ 
1.4.1 Velocidad de cálculo en función de la longitud 
de los vectores 
1.4.1.1 R∞ y N1/2 
Recordemos cómo se puede expresar el tiempo de ejecución de un 
programa en modo vectorial y en modo escalar: 
▪ en modo escalar 
 TE = te N te = tiempo para ejecutar una iteración 
 
▪ en modo vectorial7 
 TV = ti + tv N ti = tiempo de inicio 
 tv = tiempo para procesar un elemento del vector 
 
Los tiempos de ejecución se suelen dar en ciclos o en (nano)segundos 
(basta multiplicar por el periodo del reloj). En la figura se representa el 
tiempo de ejecución vectorial en función de la longitud de los vectores, una 
recta. 
 
 
 
 
 
 
 
 
 
A partir de ahí, definimos la velocidad de cálculo o rendimiento 
(performance) como el número de elementos que se procesa por unidad de 
tiempo (ciclo o segundo): 
Ntt
N
T
NR
viV
N +
==
 
La velocidad de cálculo se suele dar en Mflop/s (Mega FLoat OPeration / 
second), es decir, cuántas operaciones de coma flotante se realizan por 
segundo, para lo que tenemos que considerar el número de operaciones que 
 
7 Sin considerar el tamaño de los registros vectoriales. 
0 
5 
100 
150 
200 
250 
300 
0 25 50 75 100 125 150 
N (longitud de los vectores) 
ti 
pendiente = tb 
 TV = 30 + 2N 
N1/2 
2ti 
 TV 
▪ 32 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
se realizan con los vectores, OpCF8, ya que en total se ejecutarán N × OpCF 
operaciones. 
El tiempo de ejecución debe estar en segundos; si está en ciclos (como en 
los ejemplos vistos hasta ahora) hay que multiplicarlo por el periodo de reloj, 
lo que, dado que el tiempo está en el divisor, equivale a multiplicar la 
expresión anterior por la frecuencia de reloj (T = 1/F). Por tanto: 
 
FOpCF
Ntt
N
T
NR
viV
N ××+
== Mflop/s (TV en ciclos, F en MHz) 
 
Analicemos gráficamente el comportamiento de la expresión anterior. 
 
 
 
 
 
 
 
 
 
Tal como se observa en la figura, la función R tiene una asíntota cuando N 
tiende a ∞. Aunque los vectores fueran muy largos, la velocidad de cálculo 
tiene un límite. A ese valor máximo se le denomina R∞. 
FOpCF
t
RR
v
NN ××== ∞→∞
1lim
 
También es habitual indicar la eficiencia del sistema, es decir, la fracción 
de la velocidad máxima que se consigue: 
 
Eficiencia = R / R∞ en el intervalo [0, 1] 
 
Por definición, ninguna máquina puede alcanzar la velocidad máxima R∞. 
Por ello, se suele utilizar otro parámetro más: N1/2, tamaño mínimo de los 
vectores que permite alcanzar al menos la mitad de la velocidad máxima. De 
acuerdo a la definición, R(N1/2) = R∞/2; por tanto: 
 
N1/2 / (ti + tv × N1/2) = 1/tv × 1/2 → N1/2 = ti / tv (entero superior) 
 
8 En lo que a la velocidad de cálculo respecta, no es lo mismo efectuar una suma con los vectores que 
efectuar dos sumas y una multiplicación. 
N (número de elementos) 
R∞ 
R
 (
re
nd
im
ie
nt
o)
 
R∞/2 
N1/2 
1.4 VELOCIDAD DE CÁLCULO DE LOS COMPUTA-DORES VECTORIALES ▪ 33 ▪ 
¡Si N1/2 es muy alto, lo más probable es que andemos lejos del valor 
máximo de velocidad; en cambio, si es pequeño, no necesitaremos procesar 
vectores muy largos para acercarnos a la velocidad máxima de cálculo. De la 
misma manera que hemos definido N1/2, podemos definir N3/4, N1/4, etc., es 
decir, el tamaño mínimo de los vectores para conseguir una determinada 
fracción de la velocidad máxima9. 
 
Los dos parámetros que definen la velocidad de cálculo (performance) pueden 
obtenerse mediante un experimento muy simple. Se ejecuta el programa vectorial 
para diferentes valores de N, se mide el tiempo de ejecución, y se dibuja la función 
TV(N), que deberá ser una recta, ya que el tiempo crece linealmente con N. La 
ordenada en el origen de esa recta nos indica el valor de ti y la pendiente de la recta 
el valor de tv. Así pues, el valor de R∞ será el inverso de la pendiente de la recta. Para 
calcular N1/2 basta con medir el valor de N que hace TV = 2 ti (cuando N = N1/2 = ti / tv, 
TV = ti + (ti /tv) tv = 2 ti). 
 
El parámetro R∞ es función del programaque se ejecuta. En todo caso, es 
sencillo obtener la velocidad teórica máxima (peak performance) que podría 
conseguir un computador vectorial. El máximo lo obtendríamos si tv = 1 
ciclo y se utilizaran simultáneamente todas las unidades funcionales (OpCF 
= #UF). Por ejemplo, un computador vectorial que dispone de 6 unidades 
funcionales y cuyo reloj es de F = 500 MHz, podría lograr 6 × 500 = 3.000 
Mflop/s = 3 Gflop/s. Sin embargo, ese valor no es representativo de un caso 
real, ya que sólo se puede conseguir en casos muy excepcionales. Lo más 
habitual es que tv > 1 y que no se estén utilizando todas las unidades 
funcionales simultáneamente. 
 
1.4.1.2 Speed-up o factor de aceleración 
Para representar la velocidad de cálculo de un computador vectorial 
podemos efectuar esta otra comparación: ¿cuántas veces es más rápida la 
ejecución del programa en modo vectorial que en modo escalar? 
 
Ntt
Nt
T
TK
vi
e
V
E
V +
== 
v
e
t
tK =∞ 
 
El comportamiento de la función KV es similar al de R, y obtiene un 
máximo cuando N tiende a infinito. El parámetro K∞ indica cuántas veces es 
 
9 Si se prefiere, el tiempo de ejecución y la velocidad de cálculo pueden darse en función de los dos 
parámetros que acabamos de definir, N1/2 y R∞: 
 
 TV = (N + N1/2) / R∞ RV = R∞ × (1 / (1 + N1/2/N)) 
▪ 34 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
más rápido el proceso de un elemento del vector en modo vectorial que en 
modo escalar, siendo los vectores que se procesan muy largos. Inicialmente, 
nos interesa que el parámetro K∞ sea grande, porque indica que el procesador 
vectorial es muy rápido. Sin embargo, como vamos a ver enseguida, la 
situación no es tan clara como parece, ya que habrá que ejecutar también 
código escalar junto con el vectorial. 
1.4.1.3 NV 
Los dos parámetros de “calidad” más utilizados son R∞ y N1/2, aunque 
pueden plantearse otros. Por ejemplo, ¿se obtiene siempre un tiempo de 
ejecución menor en modo vectorial que en modo escalar? Podemos calcular 
el parámetro Nv, longitud de los vectores que hace que TE = TV. 
 
1
2/1
−
=
−
=→+=
∞K
N
tt
tNNttNt
ve
i
vvvive 
Por tanto, si los vectores a procesar son más cortos que Nv (función de N1/2 
y K∞), entonces no merece la pena ejecutar en modo vectorial, ya que la 
ejecución en modo escalar será más rápida. 
1.4.2 Influencia del código escalar. Ley de Amdahl. 
Los programas adecuados para ejecutar en un procesador vectorial son los 
que procesan vectores. Sin embargo, en un programa general habrá que 
procesar también, junto al código vectorial, código escalar (no son habituales 
los programas que se pueden expresar en un 100% en forma de código 
vectorial). Por tanto, para medir correctamente la velocidad de ejecución, es 
necesario contar con el tiempo de ejecución del código escalar. 
Sea f la fracción de código que puede ser ejecutada vectorialmente y 1 – f 
la parte que hay que ejecutar en modo escalar. El tiempo de ejecución del 
programa se puede expresar como: 
 
TVE = f TV + (1 – f) TE 
 
Así pues, comparado con el procesador escalar, el factor de aceleración 
logrado será: 
 
KKf
K
Tf
K
Tf
T
TffT
T
T
TK
E
E
E
EV
E
VE
E
VE +−
=
−+
=
−+
==
)1()1()1(
 
1.4 VELOCIDAD DE CÁLCULO DE LOS COMPUTA-DORES VECTORIALES ▪ 35 ▪ 
La expresión anterior se conoce como ley de Amdahl. Cuando f = 0 (todo 
el código es escalar), el factor de aceleración es 1; y cuando f = 1 (todo el 
código es código vectorial), el factor de aceleración es KV, tal como hemos 
definido anteriormente. Analicemos gráficamente el comportamiento del 
factor de aceleración en función de f. 
 
 
 
 
 
 
 
 
 
 
 
 
Tal como aparece en la gráfica, para poder obtener factores de aceleración 
significativos es necesario que el factor de vectorización sea alto. Por 
ejemplo, para KV = 16, si queremos que la ejecución vectorial sea 8 veces 
más rápida, se necesita que f > 93%. Analizado desde otro punto de vista, si, 
por ejemplo, f = 0,65, el factor de aceleración no será nunca mayor que 3, 
aunque KV sea infinito; para KV = ∞, el factor de aceleración es 1 / (1–f). 
La vectorización del código de un determinado programa es tarea del 
compilador (con la colaboración, tal vez, del programador). En la gráfica 
anterior hemos marcado en el eje X los valores de f que suelen lograr los 
compiladores vectoriales, normalmente en el intervalo [0,55 - 0,75]. Queda 
claro que con esos factores de vectorización no es posible lograr altos 
valores de speed-up, aunque KV sea muy grande. Por ello, la eficiencia del 
proceso de compilación vectorial es crucial para poder obtener el máximo 
rendimiento de un computador vectorial. 
Tal como hemos calculado antes el parámetro N1/2, en este caso se puede 
obtener un parámetro similar, f1/2, factor de vectorización necesario para 
obtener al menos la mitad de la velocidad máxima (KV /2). 
 
1
11
)1(2 2/12/1 −
−=→
+−
=
VVV
VV
K
f
KKf
KK
 
KV = ∞ 
16 
 
 
 
 
 
 
 
8 
 
 
 
4 
 
2 
▪ 36 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
En general, por tanto, si se desea calcular los Mflop/s que realmente se 
conseguirán con un determinado programa, hay que considerar tanto el 
código escalar como el vectorial: 
 
NtKfNttf
N
NtfNttf
N
TffT
N
T
NR
vvieviEVVE
VE
∞−++
=
−++
=
−+
==
)1()()1()()1( 
 
Como siempre, para ponerlo en Mflop/s hay que multiplicar por el número 
de operaciones en coma flotante realizadas, y por la frecuencia de reloj (si el 
tiempo estaba en ciclos). 
 
En algunos textos, la expresión anterior suele darse de la siguiente manera: 
 
RN,f = R∞ × εN × εf donde εN = 1 / [1 + N1/2/N] y εf = 1 / [f + (1–f) K∞ × εN] 
 
es decir, hay un rendimiento máximo —R∞,1— cuando la longitud de los vectores es infinita y 
el factor de vectorización es 1; y luego existen dos limitaciones, una debida a la longitud finita 
de los vectores, N, y otra debida a que el factor de vectorización es f y no 1. 
Pongamos un ejemplo. Un computador vectorial tiene los siguientes parámetros: R∞ = 800 
Mflop/s, N1/2 = 60, K∞ = 10, f = 0,8 y N = 128. 
Si fueran N infinito y f = 1, se obtendrían 800 Mflop/s. Como N = 128, el primer límite es εN = 
0,68. Además, como f = 0,8 (y N = 128) tenemos un segundo límite, εf = 0,46. En 
consecuencia, la velocidad de cálculo que se consiga será: 800 × 0,68 × 0,46 = 252 Mflop/s. 
 
Conviene enfatizar nuevamente la influencia del código escalar en el 
rendimiento total del sistema. La siguiente figura presenta la comparación de 
dos mejoras efectuadas en un computador vectorial. 
 
 
 
 
 
 
 
 
 
 
 
El comportamiento en los dos extremos es claro. Cuando los programas se 
vectorizan por completo (f = 1), el factor de aceleración es mejor en la 
0 
2 
4 
6 
8 
10 
12 
14 
16 
0 0.2 0.4 0.6 0.8 1 
Fa
ct
or
 d
e 
ac
el
er
ac
ió
n 
(n
or
m
al
iz
ad
o)
 
 f (factor de vectorización) 
Ley de Amdahl 
CRAY X-MP 
 tv = 10 ns 
 te = 66,6ns 
 
 
 
tv = 5 ns 
te = 66,6 ns 
 
tv = 10 ns 
te = 33,3 ns 
 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 37 ▪ 
máquina en la que se ha duplicado KV (la nueva máquina es dos veces más 
rápida). En cambio, si el programa no se puede vectorizar (f = 0, todo código 
escalar), entonces los resultados son mejores en el computador que ha 
mejorado el procesador escalar. 
¿Y en un caso general? La respuesta depende de f. Pero atención, si f se 
mantiene en el intervalo [0,6 – 0,8], en ese caso no interesa que KV sea muy 
alto, y resulta más eficaz mejorar la respuesta del procesador escalar. Por 
tanto, salvo que sepamos que nuestros programas se vectorizan siempre en 
un factor muy elevado, no resulta interesante que el computador vectorial 
sea de KV muy elevado, puesto que no vamos a poder aprovechar sus 
características específicas. 
 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR 
CÓDIGO VECTORIAL 
Los procesadores vectoriales ejecutan código vectorial, pero, en general, 
sólo una parte de los programas puedeejecutarse de esa manera. ¿A quién 
corresponde detectar qué partes del código son vectorizables y escribir el 
correspondiente código, al programador o al compilador? Lo más adecuado 
es que el trabajo del programador sea independiente de la máquina; se 
programan algoritmos en alto nivel, y el correspondiente compilador 
traducirá esos programas al código más adecuado para la máquina en que se 
vayan a ejecutar, teniendo en cuenta las características de la misma. 
Afortunadamente, existen buenos compiladores vectoriales que generan 
código vectorial de manera eficiente, para lo que previamente analizan las 
dependencias entre instrucciones y deciden qué partes del código pueden 
ejecutarse vectorialmente. En todo caso, siempre es importante la ayuda de 
un programador “inteligente”, ya que a veces no es sencillo traducir 
automáticamente de alto nivel a código vectorial. Por ello, algunos lenguajes 
(Fortran, por ejemplo) tienen directivas especiales para indicar operaciones 
vectoriales y ayudar al compilador. 
Como hemos comprobado, es esencial conseguir factores de vectorización 
altos. En caso contrario, la velocidad de procesamiento se alejará mucho de 
los máximos teóricos. En los siguientes apartados vamos a analizar las 
estrategias principales que sigue un compilador vectorial para generar código 
vectorial. Utilizaremos las mismas o parecidas estrategias un poco más 
adelante, cuando tengamos que ejecutar un bucle entre P procesadores. 
▪ 38 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
1.5.1 Dependencias de datos entre instrucciones 
Como ya hemos comentado, vectorizar implica, entre otras cosas, una 
determinada reordenación del código original. Sin embargo, las 
instrucciones no pueden reordenarse de cualquier manera, puesto que hay 
que respetar las dependencias de datos. Recordemos brevemente los tres 
tipos de dependencias de datos. 
 
• Dependencias verdaderas (RAW read-after-write, RD) 
 
1: A = B + C 
2: D = A 
 
 
 Existe una dependencia entre las instrucciones 1 y 2, porque el 
resultado de la instrucción 1 se utiliza en la 2. Representamos las 
dependencias en un grafo, el grafo de dependencias, mediante una 
flecha que va de 1 a 2, indicando qué se debe hacer antes y qué 
después. En este ejemplo, la instrucción 1 debe escribir antes que la 
instrucción 2 lea el operando. 
 Las dependencias RAW no pueden evitarse, puesto que son 
intrínsecas al algoritmo que se quiere ejecutar. En algunos casos, 
pueden resolverse mediante cortocircuitos; en caso contrario, habrá 
que esperar a que se ejecute la operación anterior. 
 
• Antidependencias (WAR write-after-read, DR) 
 
 
1: A = B + C 
2: B = D 
 
 
 Existe una antidependencia entre las instrucciones 1 y 2, puesto que 
un operando que necesita la instrucción 1 –B– es modificado por la 2. 
En el grafo de dependencias las antidependencias se indican mediante 
una flecha cruzada por una raya. En este ejemplo, la flecha indica que 
se debe leer B en la instrucción 1 antes que escribir B en la 2. 
 Las antidependencias no son dependencias “fuertes”, y en muchos 
casos desaparecen con una correcta reordenación del código. 
1 
2 
A 
1 
2 
B 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 39 ▪ 
• Dependencias de salida (WAW write-after-write, RR) 
 
1: A = B + C 
2: A = D 
 
 
 Existe una dependencia de salida entre las instrucciones 1 y 2, ya que 
la instrucción 2 va a escribir en la misma variable que la 1. La 
representamos en el grafo mediante una flecha con un pequeño 
círculo, que indica que hay que respetar el orden de las escrituras. 
 Como en el caso anterior, las dependencias de salida no son “fuertes” 
y suelen estar asociadas a la manera de escribir el programa; por tanto, 
pueden desaparecer con una correcta ordenación del código vectorial. 
 
Recuerda: sea cual sea el tipo, las dependencias implican un orden 
determinado de ciertas operaciones: qué hay que hacer antes y qué después. 
 
En los párrafos anteriores hemos visto las dependencias entre 
instrucciones individuales. Pero el código vectorial reemplaza un bucle 
completo de instrucciones, por lo que al analizar las dependencias entre 
instrucciones hay que tener en cuenta que se pueden producir entre 
instrucciones de cualquier iteración. Definimos “distancia” de una 
dependencia como el número de iteraciones que hay entre las instrucciones 
que tienen dicha dependencia, y la indicaremos en el propio grafo de 
dependencias. Por ejemplo, si la dependencia se produce en la misma 
iteración, la distancia es 0; si es en la siguiente, la distancia es 1, etc. En los 
bucles de más de una dimensión, la distancia se representa como un vector 
de distancias, con un elemento por cada dimensión del bucle. Por ejemplo: 
 
do i = 2, N-2 
1: A(i) = B(i) + 2 
2: C(i) = A(i-2) + A(i+1) 
enddo 
 
do i = 2, N-1 
do j = 1, N-2 
1: A(i,j) = A(i,j-1) * 2 
2: C(i,j) = A(i-2,j+1) + 1 
enddo 
enddo 
 
grafo de dependencias espacio de iteraciones 
1 
2 
A 
A, 2 
A, 1 
i 
i=0 i=1 i=2 … 
1 
2 
A, 2 A, 1 
1 
2 
A, (2, –1) 
A, (0, 1) 
j 
i A, (2, –1) 
A, (0, 1) 
▪ 40 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
Cuando la dependencia se produce entre iteraciones diferentes (d > 0), se 
dice que es loop carried. 
Para poder vectorizar un bucle, el primer paso consiste en efectuar el 
análisis de dependencias (no olvides que vectorizar implica desordenar el 
código), y generar el grafo de dependencias. En este grafo se representan 
las dependencias entre instrucciones —de la instrucción i a la j—, para todas 
las iteraciones del bucle. En los casos de más de una dimensión, también es 
útil dibujar un segundo grafo, el espacio de iteraciones (como en la figura 
anterior), en el que las dependencias no se marcan entre instrucciones, sino 
entre iteraciones. En los siguientes ejemplos utilizaremos ambos grafos. 
1.5.2 Vectorización 
1.5.2.1 Vectores de una dimensión 
Antes de formalizar las técnicas de vectorización, veamos algunos 
ejemplos10. 
1.5.2.1.1 Primer ejemplo 
 
do i = 0, N-1 
 A(i) = B(i) + C(i) 
enddo 
 
No existe ningún tipo de dependencia entre las instrucciones del bucle, 
por lo que puede escribirse en forma vectorial, sin problemas: 
 
MOVI VL,#N ; longitud de los vectores 
MOVI VS,#1 ; paso de los vectores (stride) 
 
LV V1,B(R1) 
LV V2,C(R1) 
ADDV V3,V1,V2 
SV A(R1),V3 
 
En algunos lenguajes, el código anterior se expresa así: A(0:N:1) = 
B(0:N:1) + C(0:N:1), donde A(x:y:z) indica: x, comienzo del vector; 
y, número de elementos; y z, paso del vector. 
 
10 Salvo que se indique lo contrario, los vectores son de tamaño N (o N×N); la dirección A indica el 
primer elemento del vector, A0; A+1 indica el siguiente elemento, etc. (sin considerar el tamaño de 
los elementos y la unidad de direccionamiento de la memoria). Vectores de nombre diferente utilizan 
posiciones de memoria diferentes, es decir, no se solapan (no hay aliasing). El contenido inicial del 
registro utilizado para direccionar es siempre 0 (en el ejemplo, R1). 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 41 ▪ 
El código escalar original y el vectorial que acabamos de escribir no son 
exactamente equivalentes. El bucle escalar utiliza la variable i para 
controlar el número de iteraciones e indicar los elementos del vector, por lo 
que al acabar el bucle i contendrá el valor correspondiente a la última 
iteración, y, aunque no es habitual, tal vez se utilice dicha variable más 
adelante en el programa. El compilador vectorial tiene que generar código 
equivalente al original; por ello, aunque no se necesita para nada en las 
instrucciones vectoriales, debe dejar en i el valor final correspondiente, en 
este caso N–1. Lo mismo habrá que hacer con el resto de variables similares. 
Por claridad, vamos a omitir el código correspondiente a esas operaciones. 
1.5.2.1.2 Segundo ejemplo 
 
 
do i = 0, N-1 
1: A(i) = B(i)+ C(i) 
2: D(i) = A(i) 
enddo 
 
 grafo de dependencias espacio de iteraciones 
 
Hemos dibujado ambos grafos: el de dependencias y el del espacio de 
iteraciones. Existe una dependencia en el bucle, de la primera instrucción a 
la segunda, y la dependencia se produce en la misma iteración, como se 
observa en el espacio de iteraciones. 
En algunos textos, la dependencia anterior se indica de la siguiente manera: 1 δ= 2; 
el símbolo = indica que la dependencia es de distancia 0 (si la distancia es mayor 
que 0, se utiliza el símbolo <). 
 
La dependencia no implica ningún problema, y el código se puede 
vectorizar de la siguiente manera: 
 
 MOVI VL,#N 
 MOVI VS,#1 
 
(1) LV V1,B(R1) 
 LV V2,C(R1) 
 ADDV V3,V1,V2 
 SV A(R1),V3 ; A = B + C 
 
(2) SV D(R1),V3 ; no hay que leer A, ya que está en V3 
1 
2 
A, 0 
A, 0 
i 
▪ 42 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
1.5.2.1.3 Tercer ejemplo 
 
do i = 1, N-1 
1: A(i) = B(i) + C(i) 
2: D(i) = A(i-1) 
enddo 
 1 δ< 2 
 
Aunque el grafo de dependencias es similar al del ejemplo anterior, ahora 
las dependencias van de iteración a iteración: hay que utilizar en la segunda 
instrucción de la iteración i el resultado de la primera instrucción de la 
iteración i–1. Aunque se observa una cadena de dependencias en el espacio 
de iteraciones, las dependencias son entre instrucciones diferentes: 1i → 2i+1. 
Nuevamente, el código puede vectorizarse sin problemas: 
 
 MOVI VL,#N-1 
 MOVI VS,#1 
 
(1) LV V1,B+1(R1) 
 LV V2,C+1(R1) 
 ADDV V3,V1,V2 
 SV A+1(R1),V3 ; se escibe el vector A1–AN-1 
 
(2) LV V4,A(R1) ; se lee de memoria el vector A0–AN-2 
 SV D+1(R1),V4 
 
Claramente, en esta ocasión no se puede aprovechar en la segunda 
instrucción el resultado de la primera, ya que no se trata del mismo vector 
(A0-AN-2); por tanto, primero hay que escribir en memoria el vector A1-AN-1 y 
luego leer A0-AN-2. 
1.5.2.1.4 Cuarto ejemplo 
 
 
 
 
do i = 0, N-2 
1: A(i) = B(i) + C(i) 
2: D(i) = A(i+1) 
enddo 
 
En este bucle existe una antidependencia, de la segunda instrucción a la 
primera. El bucle no puede vectorizarse en el orden original, puesto que no 
se puede escribir el vector A(i) (todos los elementos) en la primera 
instrucción antes que leer el vector A(i+1) en la segunda. 
1 
2 
A, 1 
A, 1 
i 
1 
2 
A, 1 
A, 1 
i 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 43 ▪ 
Formalizaremos este caso un poco más adelante; basta ahora decir que el 
problema se arregla con un cambio de orden, tal como el siguiente: 
 
 MOVI VL,#N-1 
 MOVI VS,#1 
 
(2) LV V1,A+1(R1) ; adelantar la lectura de la instrucción 2 
 SV D(R1),V1 
 
(1) LV V2,B(R1) 
 LV V3,C(R1) 
 ADDV V4,V2,V3 
 SV A(R1),V4 ; escribir el resultado de la instrucción 1 
 
Como puede observarse, el código vectorial respeta la antidependencia 
original. 
 
1.5.2.1.5 Quinto ejemplo 
 
 
do i = 1, N-1 
1: A(i) = B(i-1) + 1 
2: B(i) = A(i) 
enddo 
 
En este bucle aparece el problema más grave de vectorización. Las 
dependencias forman un ciclo en el grafo: la instrucción 2 necesita los datos 
producidos por la 1 (vector A), y la primera instrucción necesita los datos 
producidos por la segunda (casi todo el vector B). No hay nada que hacer; 
hay que ejecutar el bucle en modo escalar. 
El ejemplo más típico de un ciclo de dependencias es una recurrencia: un 
ciclo de una única instrucción. Por ejemplo: 
 
do i = 3, N-1 
1: A(i) = A(i-3) * 3 
enddo 
 
En cada iteración, se necesita como operando el resultado producido tres 
iteraciones antes. Está claro que una recurrencia no puede vectorizarse: 
¿cómo leer con una instrucción —LV V1,A(R1)— todo un vector, si 
todavía no se han generado los elementos del vector? 
1 
2 
A, 0 B, 1 
A, 0 
i 
B, 1 
1 A, 3 
▪ 44 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
1.5.2.2 Vectores de N dimensiones 
Los vectores de los ejemplos anteriores son de una dimensión. Pero, 
¿cómo se vectoriza, por ejemplo, una operación con matrices? 
 
do i = 0, N-1 (todos los vectores son de tamaño [N, M]) 
 do j = 0, M-1 
 A(i,j) = B(i,j) + C(i,j) 
 enddo 
enddo 
 
Cuando se trabaja con matrices, se suelen utilizar habitualmente dos tipos 
de vectores: filas y columnas. El propio bucle indicará cómo hay que 
procesar la matriz, por filas o por columnas, pero, en muchos casos, ambas 
posibilidades son correctas (como en el caso anterior, en el que da igual 
ejecutar el bucle en el orden do i / do j que en el orden do j / do i). 
Para generar el grafo de dependencias, el compilador analizará el bucle 
más interior, y en base a ello decidirá qué hacer. En todo caso, para procesar 
vectores de dos dimensiones es necesario montar un bucle escalar, que 
procese las filas, o las columnas, una a una. 
Por ejemplo, en el bucle anterior no hay ninguna dependencia. Por tanto, 
tenemos dos posibilidades: vectorizar el bucle interior —j, procesar la 
matriz por filas—, o el exterior —i, por columnas—. 
Si vectorizamos por filas, el bucle quedaría así: 
 
 MOVI R2,#N ; número de filas 
 MOVI VL,#M ; longitud de las filas 
 MOVI VS,#1 ; paso 
 
buc: LV V1,B(R1) 
 LV V2,C(R1) 
 ADDV V3,V1,V2 
 SV A(R1),V3 
 
 ADDI R1,R1,#M ; siguiente fila 
 SUBI R2,R2,#1 ; una fila menos 
 BNZ R2,buc 
 
Después de procesar un fila vectorialmente, se actualiza el registro R1 
(+M), para direccionar la fila siguiente. El registro R2 es un simple contador, 
para procesar todas las filas. 
j 
A 
s = 1 
i 
0,0 0,1 … 0,M-1 
1,0 1,1 … 1,M-1 
… … … … 
N-1,0 N-1,1 … N-1,M-1 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 45 ▪ 
Si se quiere vectorizar la matriz por columnas, el código sería el 
siguiente: 
 
 MOVI R2,#M ; número de columnas 
 MOVI VL,#N ; longitud de las columnas 
 MOVI VS,#M ; paso 
 
buc: LV V1,B(R1) 
 LV V2,C(R1) 
 ADDV V3,V1,V2 
 SV A(R1),V3 
 
 ADDI R1,R1,#1 ; siguiente columna 
 SUBI R2,R2,#1 ; una columna menos 
 BNZ R2,buc 
 
En este caso, el paso de los vectores (columnas) es M, y para apuntar a la 
siguiente columna basta con incrementar (+1) la dirección de comienzo11. 
Cuando se utiliza esta segunda opción se dice que se ha efectuado un 
intercambio de bucles. 
En general, para vectorizar bucles de P dimensiones hay que analizar P 
alternativas (una por cada dimensión), para escoger la más adecuada en 
función de las dependencias de datos entre las instrucciones. 
1.5.2.3 Condición para vectorizar un bucle 
Resumamos lo visto en los ejemplos anteriores. El compilador debe 
analizar las dependencias entre las instrucciones y generar un grafo de 
dependencias. Basándose en ello, debe decidir si el bucle es vectorizable o 
no y cómo. A menudo, para generar código vectorial es necesario reordenar 
el código original. Al hacerlo, claro está, el compilador debe respetar el 
orden de ejecución que imponen las diferentes dependencias de datos: la 
dependencia x → y indica que alguna operación de la instrucción x debe ir 
antes que alguna de la y. 
Por desgracia, no todos los bucles son vectorizables. ¿Cómo saber cuándo 
sí y cuándo no? En general, un bucle puede vectorizarse si las 
dependencias entre instrucciones no forman ciclos en el grafo de 
dependencias. 
 
11 En estos ejemplos hemos supuesto que las matrices están almacenada en memoria por filas, tal como, 
por ejemplo, se hace en C; en Fortran, en cambio, las matrices se guardan por columnas. 
j A 
s = M 
i 
0,0 0,1 … 0,M-1 
1,0 1,1 … 1,M-1 
… … … … 
N-1,0 N-1,1 … N-1,M-1 
▪ 46 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
En esos casos, el compilador generará código vectorial para el bucle, 
manteniendo en algunos casos el orden original de las instrucciones, y 
cambiándolo en otros para respetar las dependencias. La condición anterior 
no implica que no se pueda vectorizar el bucle cuando existan ciclos de 
dependencias, puesto que, como vamos a ver, pueden aplicarse ciertas 
técnicas que “deshacen” dichos ciclos. 
Aunque las dependencias formen ciclos en el grafode dependencias, 
normalmente sólo algunas instrucciones del bucle tomarán parte en dichos 
ciclos. Por ello, aunque no se puedan vectorizar todas la instrucciones del 
bucle, el compilador debe intentar vectorizar el mayor número posible de 
operaciones; las instrucciones que presenten problemas se ejecutarán 
escalarmente, y el resto vectorialmente (loop fission). Por ejemplo: 
 
 
 
do i = 1, N-1 
 A(i) = B(i) 
 B(i) = B(i-1) 
enddo 
 
 
(puede vectorizarse la primera instrucción, 
pero no la segunda, debido a la dependencia, una 
recurrencia) 
 MOVI VL,#N-1 
 MOVI VS,#1 
 
 LV V1,B+1(R1) 
 SV A+1(R1),V1 
 
 MOVI R3,#N-1 
 
buc: FLD F1,B(R2) 
 FST B+1(R2),F1 
 ADDI R2,R2,#1 
 SUBI R3,R3,#1 
 BNZ R3,buc 
 
 
1.5.2.4 Test de dependencias 
Como hemos comentado, el primer paso del proceso de vectorización es el 
análisis de las dependencias. ¿Es sencillo saber si existe una dependencia 
entre dos instrucciones dadas? En los ejemplos anteriores era muy simple, 
porque los índices utilizados para el acceso a los vectores eran funciones 
muy sencillas (i, i+1...). Sin embargo, ¿qué podemos decir en este ejemplo? 
 
do i = L1, L2 
 X(f(i)) = X(g(i)) + 1 
enddo 
 
¿Hay una dependencia en el vector X (sería una recurrencia)? Claro está, 
la respuesta depende de las funciones f y g. Por desgracia, el resultado de 
las funciones f y g no se puede predecir, en el caso general, en tiempo de 
1 
2 
B, 1 
B, 0 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 47 ▪ 
compilación, por lo que el compilador no tiene información suficiente para 
tomar una decisión, por lo que debe suponer que existe la dependencia. 
Sin embargo, en algunos casos, que son muy comunes, el compilador 
puede analizar y decidir si existe o no una dependencia: en el caso en que f 
y g sean funciones lineales de los índices del bucle. Por ejemplo: 
 
do i = L1, L2 
 X(a*i+b) = ... 
 ... = X(c*i+d) 
enddo 
 
Por otro lado, ése es el único caso que se corresponde con la definición 
que hemos dado de vector: la distancia entre dos elementos consecutivos es 
constante. Más adelante veremos cómo procesar vectores cuyo paso no sea 
constante (por ejemplo, A(i2)→ A1, A 4, A 9, A16...). 
Para saber si existe una dependencia en el vector X hay que resolver la 
siguiente ecuación: 
 
a i1 + b = c i2 + d L1 ≤ i1, i2 ∈ Z ≤ L2 
 
es decir, hay que saber si existen dos valores i1 e i2, dentro de los límites de 
iteración del bucle, para los que coincidan las direcciones de acceso al 
vector. 
 
 
 
 
 
 
 
La expresión anterior es una ecuación diofántica, y encontrar una solución 
general a la misma es muy complejo. Sin embargo, puede afirmarse que: 
 
▪ No existe dependencia (la ecuación anterior no tiene solución), 
si (d – b) / MCD(a, c) ∉ Z, es decir, si no es un entero. 
 
Este test se conoce como el test del máximo común divisor (MCD). No es 
el único test que aplican los compiladores para analizar las dependencias, 
pero es suficiente para los casos más habituales. 
a i + b 
c i + d 
i1 i2 L1 L2 
▪ 48 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
El test del MCD indica cuándo no hay dependencia, no cuándo la hay. 
Esto es, si el resultado es un número entero, las ecuaciones tienen solución, 
pero para saber si existe, o no, dependencia habrá que analizar las soluciones 
y comprobar que se encuentran dentro de los límites del bucle. Para ello, a 
menudo es suficiente con analizar los trozos del vector que accede cada 
instrucción: si no se solapan, entonces no hay dependencia; pero si se 
solapan, entonces sí que puede haberla y habrá que hacer un análisis más 
detallado12. Se pueden diferenciar tres casos: 
 
 
 
 
 
 (1) (2) (3) 
 
En el primer caso, no hay dependencia; es decir, la hipotética solución de 
la ecuación está fuera de los límites del bucle (dentro de los límites, las dos 
ecuaciones no proporcionan nunca el mismo valor). En el segundo caso, 
puede haber dependencia, ya que las dos ecuaciones tienen un trozo de 
vector común al que acceden (el tipo de dependencia variará en función del 
tipo de operación de cada acceso). El tercer caso es el más complejo. Puede 
existir dependencia entre las dos instrucciones; además, si en una se lee y en 
la otra se escribe, en un tramo del bucle tendremos antidependencias y en el 
otro, dependencias (si las dos operaciones fueran escrituras tendríamos un 
problema similar). Por tanto, estas instrucciones no se pueden vectorizar, 
algunos elementos hay que leer antes de escribir sobre ellos y otros después 
de que se hayan escrito (quizás se pueda dividir el bucle en dos partes, en 
función del punto de cruce, y utilizar técnicas distintas en cada parte para 
generar el código). 
Veamos algunos ejemplos: 
(1) do i = 1, 100 
 A(2*i) = ... → (1 – 0) / MCD(2, 2) = 1/2 
 ... = A(2*i+1) 
 enddo 
 
Por tanto, no hay dependencias entre las dos instrucciones; en este caso, 
una instrucción escribe elementos pares y la otra lee elementos impares. 
 
12 Habrá que tener en cuenta los pasos de los vectores (a y c) y la longitud del segmento que se solapa, 
para comprobar si ambos accesos coinciden en, al menos, un elemento del vector. 
i L1 L2 
i L1 L2 
i 
L1 L2 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 49 ▪ 
(2) do i = 5, 100 
 A(i-5) = ... 
 ... = A(2*i+90) → (90 – (–5)) / MCD(2, 1) = 95 
 enddo 
 
Por tanto, puede haber una dependencia. Pero no la hay, porque los 
intervalos de acceso son disjuntos: 
 wr: A0 ... ... A95 
 rd: A100 ... ... A290 
 
(3) do i = 1, 100 
 A(3*i+100) = ... → (100 – (–1)) / MCD(3, 2) = 101 
 ... = A(2*i-1) 
 enddo 
 
Podría haber una dependencia; los intervalos de acceso son los siguientes: 
 wr: A103 ... ... A400 
 rd: A1 ... ... A199 
Los dos intervalos tienen un trozo en común, por lo que puede haber una 
dependencia; y en este caso la hay: por ejemplo, la escritura de la 
iteración i = 1 en (A103) se lee en la iteración i = 52. 
 
(4) do i = 1, 100 
 A(6*i+3) = ... → (81 – 3) / MCD(6, 3) = 26 
 ... = A(3*i+81) 
 enddo 
 
Por tanto, puede haber una dependencia. Los intervalos de acceso son los 
siguientes: 
 wr: A9 ... ... A603 
 rd: A84 ... ... A381 
 
Un intervalo está dentro del otro; si existe dependencia, seguramente será 
de dos tipos. Por ejemplo, en la iteración i = 2 se lee el elemento A87, que 
luego se va a escribir (una antidependencia); pero, en la iteración i = 28, 
se lee el elemento A165, que es el resultado de la escritura de la iteración i 
= 27 (una dependencia verdadera). 
 
Atención. El paso de los accesos a memoria se puede indicar en dos 
sitios: en la definición de los límites del bucle y en las propias instrucciones. 
Antes de aplicar el test MCD, es necesario normalizar el bucle, efectuando 
un cambio de variable que haga que el paso del bucle sea 1. Por ejemplo: 
 
do i = 1, 100, 2 do k = 1, 50, 1 
 A(i) = ... A(2*k-1) = ... 
 B(2*i+5) = ... B(4*k+3) = ... 
enddo enddo 
▪ 50 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
En resumen: el compilador vectorial analiza las dependencias entre 
instrucciones del bucle y genera el correspondiente grafo. Si no existen 
ciclos en dicho grafo, no habrá problemas para vectorizar el bucle; en caso 
contrario, se intentará aplicar algunas técnicas sencillas que permiten reducir 
o anular el impacto negativo de las dependencias y/o vectorizar parcialmente 
el bucle. Analicemos, por tanto, las principales técnicas de optimización. 
1.5.3 Optimizaciones 
El proceso de compilación es esencial en la obtención de altas velocidades 
de cálculo en un computador vectorial. No hay que olvidar que de no obtener 
un factor de vectorización elevado el rendimiento de la máquina será 
bastante bajo (ley de Amdahl). Acabamos de ver cuál es la condición que 
hay que cumplir para poder vectorizar un bucle: que no haya ciclos de 
dependencias. En todo caso, algunas de las dependencias que aparecen en los 
bucles no son intrínsecas a la operación que serealiza, sino que están 
relacionadas con la manera en que se indica dicha operación (por ejemplo, 
las antidependencias o las dependencias de salida). En esos casos, es posible 
efectuar pequeñas transformaciones del código original que facilitan la 
vectorización final. Vamos a ver dos tipos de optimizaciones: las que ayudan 
a que desaparezcan las dependencias, y las que ayudan a obtener una mayor 
velocidad de cálculo. 
1.5.3.1 Sustitución global hacia adelante (global forward substitution) 
Analicemos este bucle: 
 
NP1 = L + 1 
NP2 = L + 2 
... 
do i = 1, L 
1: B(i) = A(NP1) + C(i) 
2: A(i) = A(i) - 1 
 do j = 1, L 
3: D(j,NP1) = D(j-1,NP2) * C(j) + 1 
 enddo 
enddo 
 
¿Existe una antidependencia entre las instrucciones 1 y 2? ¿Hay una 
recurrencia en la instrucción 3? 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 51 ▪ 
Las dos definiciones, NP1 y NP2, que se han hecho antes del bucle son un 
obstáculo para poder tomar una decisión. Por ello, antes que nada, el 
compilador deshará ambas definiciones en todo el programa, sustituyendo 
las variables por su valor original (una constante), y entonces hará el análisis 
de dependencias. Recuerda: si no puede analizar los índices de los vectores, 
el compilador debe asumir que sí existe la dependencia. 
 
do i = 1, L 
1: B(i) = A(L+1) + C(i) 
2: A(i) = A(i) - 1 
 do j = 1, L 
3: D(j,L+1) = D(j-1,L+2) * C(j) + 1 
 enddo 
enddo 
 
Ahora la decisión es clara: no existe antidependencia entre 1 y 2, porque 
los índices de los vectores (i y L+1) nunca serán iguales; de la misma 
manera, no existe recurrencia en la instrucción 3, porque L+1 ≠ L+2. 
Esta técnica se aplica para deshacer la definición de cualquier constante. 
 
1.5.3.2 Eliminación de las variables de inducción 
Analicemos este bucle: 
 
j = 2 
k = 2 
do i = 1, L 
 j = j + 5 
 R(k) = R(j) + 1 
 k = k + 3 
enddo 
 
¿Existe una recurrencia en el vector R? Tal como está escrito, el 
compilador no sabe analizar la dependencia, porque desconoce los valores de 
las variables j y k. Sin embargo, un análisis sencillo de cómo se accede a 
los vectores nos indica que no existe tal dependencia. La evolución de las 
variables j y k con relación a i es la siguiente: 
 
i = 1 2 3 4 5 ... 
j = 7 12 17 22 27 ... 
k = 2 5 8 11 14 ... 
▪ 52 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
Los valores que toman j y k forman una progresión aritmética, y no hay 
problema en redefinirlas de la siguiente manera: 
 
j = 5 i + 2 y k = 3 i – 1 
 
Las variables que forman una serie aritmética en función del índice del 
bucle se conocen como variables de inducción. Eliminando las variables de 
inducción, el bucle anterior puede escribirse así: 
 
do i = 1, L 
 R(3*i-1) = R(5*i+2) + 1 
enddo 
 
Ahora sí, un compilador vectorial puede analizar si existe una 
dependencia en R. Es bastante común encontrar variables auxiliares de este 
tipo en los bucles de cálculo, y, por tanto, el compilador tendrá que 
detectarlas y sustituirlas por las funciones correspondientes, para poder 
realizar el análisis de dependencias (y, en su caso, para poder vectorizar el 
bucle). 
1.5.3.3 Antidependencias (DR, WAR) 
Tal como ya hemos comentado, las antidependencias son dependencias 
“débiles”, y normalmente su efecto en la vectorización del código puede 
eliminarse con pequeñas transformaciones del código original. 
Por ejemplo: 
 
do i = 0, N-2 
1: A(i) = B(i) + C(i) 
2: D(i) = A(i) + A(i+1) 
enddo 
 
 
Las dependencias del bucle forman un ciclo en el grafo de dependencias. 
Por tanto, si no se hace algo, el bucle no se puede vectorizar. Pero entre las 
dependencias que forman el ciclo hay una antidependencia: la segunda 
instrucción debe leer el vector A(i+1), antes que la primera instrucción 
escriba A(i) (si no, leeríamos los valores nuevos, y no los viejos, que es lo 
que indica el programa). ¿Se puede hacer algo? Sí: leer primero el vector 
A(i+1). Basta para ello con escribir el bucle de la siguiente manera: 
2 
1 
A, 0 A, 1 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 53 ▪ 
 
do i = 0, N-2 
0: [T(i)] = A(i+1) 
1: A(i) = B(i) + C(i) 
2: D(i) = A(i) + [T(i)] 
enddo 
 
 
En la nueva versión, el grafo de dependencias del bucle no presenta 
ningún ciclo de dependencias, por lo que puede vectorizarse sin problemas. 
Normalmente no es necesario salvar en memoria el vector cuya lectura se ha 
adelantado, y basta con dejarlo en un registro, que se utilizará luego para 
ejecutar la instrucción correspondiente. Sólo si no tuviéramos un registro 
disponible llevaríamos el vector a memoria. 
Así quedará el código vectorial: 
 
 MOVI VL,#N-1 
 MOVI VS,#1 
 
(2/0) LV V1,A+1(R1) ; se aadelanta la lectura de A+1 
(1) LV V2,B(R1) 
 LV V3,C(R1) 
 ADDV V4,V2,V3 
 SV A(R1),V4 
 
(2) ADDV V5,V1,V4 ; se utiliza lo que se leyó antes (V1) 
 SV D(R1),V5 
 
1.5.3.4 Dependencias de salida (RR, WAW) 
Un caso similar al anterior se puede producir con las dependencias de 
salida, como, por ejemplo, en este bucle: 
 
do i = 0, N-3 
1: A(i) = B(i) + C(i) 
2: A(i+2) = A(i) * D(i) 
enddo 
 
El grafo de dependencias presenta un ciclo, en el que toma parte una 
dependencia de salida. Si no se efectúa alguna transformación, el bucle no es 
vectorizable. Para mantener el significado del bucle, la segunda instrucción 
tiene que efectuar la escritura antes que la primera; o, lo que es equivalente, 
hay que atrasar la escritura de la primera instrucción, así por ejemplo: 
2 
1 T, 0 
A, 1 
0 
A, 0 
1 
2 
A, 0 A,2 
▪ 54 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
 
do i = 0, N-3 
1: [T(i)] = B(i) + C(i) 
2: A(i+2) = [T(i)] * D(i) 
3: A(i) = [T(i)] 
enddo 
 
 
Como en el caso anterior, no suele ser necesario utilizar el vector auxiliar 
(T), y basta con dejar el resultado en un registro, para llevarlo más tarde a 
memoria. Así quedará el bucle vectorial: 
 
 MOVI VL,#N-2 
 MOVI VS,#1 
 
(1) LV V1,B(R1) ; instruccción 1, salvo la escritura 
 LV V2,C(R1) 
 ADDV V3,V1,V2 
 
(2) LV V4,D(R1) 
 MULV V5,V3,V4 
 SV A+2(R1),V5 
 
(1/3) SV A(R1),V3 ; escritura de la instrucción 1 
 
1.5.3.5 Intercambio de bucles (loop-interchanging) 
Los vectores de dos dimensiones (en general, de n dimensiones) pueden 
vectorizarse de más de una manera, según su definición (por filas, por 
columnas...). Para escoger una de ellas, hay que tener en cuenta las 
dependencias entre instrucciones. Por ejemplo: 
 
 
do i = 0, N-1 
 do j = 1, N-1 
 A(i,j) = A(i,j-1) + 1 
 enddo 
enddo 
 
 
Además del grafo de dependencias (sólo hay una instrucción en el bucle, 
por lo que de haber alguna dependencia será consigo misma), hemos 
dibujado las dependencias en el espacio de iteraciones, para ver cómo se 
1 
2 
3 
T, 0 
A, 2 
T, 0 
j 
i 
1 
A, (0, 1) 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 55 ▪ 
reparten en el tiempo. De dicho grafo es fácil concluir que el código no 
puede vectorizarse tal como está escrito, es decir, por filas, ya que en la 
iteración j se necesitan los resultados de la iteración j–1. Pero igualmente se 
observa que no hay inconveniente en vectorizar la operación por columnas, 
de esta manera: 
 
do j = 1, N-1 
 do i = 0, N-1 
 A(i,j) = A(i,j-1) + 1 
 enddo 
enddo 
 
 
Basta con utilizar como vector las columnas de la matriz (s = N), es decir, 
intercambiar el orden original de los bucles. 
El intercambio de bucles no puede aplicarse a cualquier bucle, ya que, por 
supuesto, hay que respetar las dependencias entre instrucciones. Por 
ejemplo, no puede aplicarse en el siguiente ejemplo: no se puede procesar la 
matriz por columnas, puesto que en la columna j se necesitan los resultados 
de la columna j+1. 
 
do i = 1, N-1 
 do j = 1, N-2 
(1) A(i,j) = B(i-1,j+1) + 1 
(2) B(i,j) = A(i,j-1) 
 enddo 
enddo 
 
Cuando se intercambia el orden de los bucles, se modifica el vector de 
distancias de las dependencias. Por ejemplo, una dependencia de distancia 
(2, 1) se convierte en otra de distancia (1, 2). La regla que permite el 
intercambioes la siguiente: el primer elemento no cero del nuevo vector 
de distancias debe ser positivo. 
Por ejemplo, para el ejemplo del grafo anterior: 
 
- sin intercambiar los bucles - tras intercambiar los bucles 
 d1 → (0, 1) d1 → (1, 0) no hay problemas 
 d2 → (1, -1) d2 → (-1, 1) esto no es posible 
 
Así pues, no se puede vectorizar por filas y no se puede intercambiar los 
bucles. 
j 
i 
j 
i 1 
2 
A, (0, 1) 
B, (1, -1) 
▪ 56 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
En algunos casos, es necesario aplicar fisión e intercambio de bucles para 
poder vectorizar el bucle. Por ejemplo, 
 
do i = 1, N-1 
 do j = 1, N-1 
(1) A(i,j) = A(i-1,j) + 1 
(2) B(i,j) = B(i,j-1) * 2 
 enddo 
enddo 
 
De acuerdo a las dependencias que aparecen en el espacio de iteraciones, 
el bucle no se puede vectorizar ni por filas ni por columnas. Pero, en este 
ejemplo, la dependencia por filas corresponde a una instrucción (2) y la de 
las columnas a otra (1), tal como vemos en el grafo de dependencias. El 
bucle lo dividiremos en dos; luego, la primera instrucción (vector A) la 
vectorizaremos por filas, y la segunda (vector B) por columnas, 
intercambiando los bucles. 
Sea como sea, sólo se intercambian los bucles si con ello se facilita la 
vectorización del código o se mejora el rendimiento. Por ejemplo, en este 
caso: 
 
do i = 0, 99 
 do j = 0, 9 
 A(i,j) = A(i,j) + 1 
 enddo 
enddo 
 
No hay dependencias entre las iteraciones, y por tanto puede vectorizarse 
por filas, tal como está escrito, o por columnas, si se cambia el orden de los 
bucles. Si se hace por filas, se procesan 100 vectores de 10 elementos. Los 
vectores son pequeños, por lo que el rendimiento no será alto (es una función 
de N). Sin embargo, si se cambia el orden se procesarán 10 vectores de 100 
elementos, con lo que se obtendrá una mayor velocidad de proceso. 
 
1.5.3.6 Expansión escalar (scalar expansion) 
Al escribir bucles es habitual utilizar variables auxiliares que facilitan la 
escritura del bucle. Por ejemplo: 
1 A, (1, 0) 
2 
B, (0, 1) 
j 
i 
A 
B 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 57 ▪ 
do i = 0, N-1 
 suma = A(i) + B(i) 
 C(i) = suma * suma 
 D(i) = suma * 2 
enddo 
 
Aunque procesamos vectores, utilizamos una variable escalar auxiliar, 
suma. Sin embargo, esa variable impide la vectorización del bucle, ya que 
genera dependencias entre todas las iteraciones. No se trata de una variable 
propia del bucle, sino de una simple variable auxiliar, así que ¿por qué no 
escribir el código de esta otra manera? 
 
do i = 0, N-1 
 suma(i) = A(i) + B(i) 
 C(i) = suma (i) * suma (i) 
 D(i) = suma (i) * 2 
enddo 
 
Lo que antes era una variable escalar ahora es un vector completo: 
suma(i). Ya no hay ningún problema para vectorizar el bucle anterior. 
Esta técnica, convertir un escalar en un vector, se conoce con el nombre de 
expansión escalar. 
Como en casos anteriores, no suele ser necesario guardar el vector auxiliar 
en memoria, sino que basta con utilizar los registros del procesador. En todo 
caso, no hay que olvidar que, al final del bucle, la variable original suma 
debe contener el valor correspondiente a la última iteración del bucle: 
 suma = suma(N-1) 
 
1.5.3.7 Fusión de bucles (loop fusion) 
Con esta optimización se intenta fundir dos (o más) bucles en uno solo, 
para intentar reducir toda la sobrecarga asociada al control del bucle, y para, 
si es posible, reutilizar los resultados almacenados en los registros. Por 
ejemplo, 
 
do i = 0, N-1 
 Z(i) = X(i) + Y(i) 
enddo 
 
do i = 0, N-1 
 R(i) = Z(i) + 1 
enddo 
do i = 0, N-1 
 Z(i) = X(i) + Y(i) 
 R(i) = Z(i) + 1 
enddo 
 
▪ 58 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
Los dos programas del ejemplo son idénticos, pero el segundo es más 
“sencillo” de ejecutar. Para empezar, el compilador puede aprovechar en la 
segunda instrucción las operaciones de la primera, leyendo el operando de 
un registro (no haremos SV Z y luego LV Z); además de ello, todo el código 
asociado con la ejecución del bucle sólo se ejecutará una vez 
(direccionamiento, longitud y paso de los vectores...). 
De todas maneras, no es seguro que el compilador efectúe esta 
optimización automáticamente, puesto que para ello debería realizar el 
análisis de dependencias más allá del bloque básico. 
En todo caso, claro está, no siempre es posible fundir dos bucles en uno, 
puesto que hay que respetar las dependencias de datos. Por ejemplo, estos 
dos programas no son iguales: 
 
do i = 1, L 
 Z(i) = X(i) + Y(i) 
enddo ≠ 
do i = 1, L 
 R(i) = Z(i+1) + 1 
enddo 
do i = 1, L 
 Z(i) = X(i) + Y(i) 
 R(i) = Z(i+1) + 1 
enddo 
 
 
1.5.3.8 Colapso de bucles (loop collapsing) 
Como ya sabemos, los bucles de varias dimensiones pueden vectorizarse 
de diferentes maneras: por filas, por columnas... Pero cuando el tamaño de 
los vectores es pequeño, puede ser interesante "juntar" dos (o más) bucles en 
uno. Por ejemplo: 
 
float A(10,10) 
 
do i = 0, 9 
 do j = 0, 9 
 A(i,j) = A(i,j) + 1 
 enddo 
enddo 
 
El bucle puede ejecutarse vectorialmente sin problemas, pero los vectores 
(filas o columnas) son muy pequeños. Para aprovechar mejor el tamaño de 
los registros vectoriales, podemos transformar el bucle de la siguiente 
manera: 
float A(10,10) 
 
do i = 0, 99 
 A(i) = A(i) + 1 
enddo 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 59 ▪ 
Como sabemos, el espacio de memoria es lineal, y las filas de la matriz se 
almacenan una tras otra; por tanto, la matriz A[N,N] puede tratarse como si 
fuera el vector A[NxN]. 
1.5.3.9 Otras optimizaciones 
Las técnicas de optimización que acabamos de ver son las más habituales, 
aunque existen otras. Sin embargo, más de una vez ocurre que lo que parece 
muy simple de vectorizar resulta muy complejo de hacer automáticamente 
(para el compilador). En esos casos, la ayuda del programador resulta el 
camino más sencillo. Esa ayuda suele efectuarse mediante pseudo-
instrucciones para el compilador, al que se le indica qué trozos de código 
debe traducir a código vectorial sin preocuparse del análisis de 
dependencias. Veamos un ejemplo: 
 
do i = a, b 
 X(i) = Y(i) + X(i+M) 
enddo 
 
Sin más información, el compilador no puede vectorizar el bucle, porque 
puede existir una recurrencia en X, en función del valor de M: si M ≥ 0, no 
hay problemas para vectorizar el bucle, pero si M < 0, el bucle no es 
vectorizable (M no es una constante; en caso contrario, el compilador la 
sustituiría por su valor). Sin embargo, puede ser que el usuario tenga 
información extra sobre la variable M. Por ejemplo, tal vez sabe que se trata 
de un parámetro físico que siempre es positivo (o que, por ejemplo, se acaba 
de ejecutar M = A(i) * A(i)). Si es así, bastaría con indicarle al 
compilador que vectorizara el bucle, sin más. 
Por otro lado, el compilador podría también ejecutar el bucle de la 
siguiente manera: 
 
 
if (M ≥ 0) then 
 do i = a, b 
 X(i) = Y(i) + X(i+M) 
 enddo 
else 
 do i = a, b 
 X(i) = Y(i) + X(i+M) 
 enddo 
endif 
 
La primera parte (then) se ejecutará como código vectorial; la segunda, 
en cambio, escalarmente. 
▪ 60 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
1.5.4 Vectores de máscara y vectores de índices 
Todas las operaciones vectoriales que hemos analizado hasta el momento 
han sido muy “simples”. Sin embargo, no siempre es ése el caso en los 
programas reales. Vamos a analizar dos casos muy habituales, que aparecen 
mucho en el cálculo científico: el uso de máscaras y los vectores de paso 
variable. 
1.5.4.1 Uso de máscaras 
En más de una ocasión, no hay que procesar todos los elementos de un 
vector, sino solamente algunos de ellos. Por ejemplo: 
 
do i = 0, N-1 
 if (B(i) > 5) then A(i) = A(i) + 1 
enddo 
 
Con lo que hemos analizado hasta el momento, no sabríamos cómo 
ejecutar vectorialmente ese bucle, pero es un caso tan habitual que tiene una 
solución específica: el uso de un registro de máscara. El registro de 
máscara (VM, vectormask) es un registro vectorial booleano (1/0) especial, 
que guarda el resultado de una operación lógica sobre vectores. Todas la 
operaciones vectoriales toman en consideración el registro VM para decidir 
qué elementos del vector hay que procesar y cuáles no. 
El procesador dispone de instrucciones específicas para trabajar con el 
registro de máscara. Por ejemplo: 
 
SxxV V1,V2 Compara dos vectores, elemento a elemento, y deja los 
resultados (1/0) en el registro de máscara VM (xx = 
operación de comparación: EQ, NE, GT...). 
 
SxxVS V1,F1 Igual, pero utilizando un escalar para la comparación. 
 
CVM Clear vector mask, para inicializar la máscara. 
 
POP R1,VM Cuenta el número de bits activados en el registro de 
máscara; deja el resultado en R1. 
 
 
Usando esas instrucciones, podemos ejecutar vectorialmente el bucle 
anterior de la siguiente manera: 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 61 ▪ 
MOVI VL,#N 
MOVI VS,#1 
MOVI F1,#5 
 
LV V1,B(R1) 
SGTVS V1,F1 ; Set Greater Than Vector/Scalar VM := V1~F1 
 
LV V2,A(R1) 
ADDVI V3,V2,#1 
SV A(R1),V3 
 
CVM ; Clear Vector Mask 
 
La instrucción SGTVS compara los elementos de V1 con el contenido de 
F1, y el resultado se deja en el registro VM. Las operaciones vectoriales 
siguientes sólo tendrán efecto en las posiciones de los vectores indicadas en 
el registro VM. La instrucción CVM inicializa nuevamente el registro VM (los 
valores concretos dependen del computador). 
Ten en cuenta que el tiempo de ejecución no cambia; cuando se ejecuta 
ADDVS, únicamente se enmascaran las escrituras en el registro destino. 
También hay que estar atentos a las posibles excepciones que se generen en 
la unidad funcional correspondiente, puesto que se tratan todos los 
elementos. En otras máquinas en cambio, se enmascara tanto la operación en 
la unidad funcional como la escritura en el registro. 
1.5.4.2 Vectores de índices 
En muchas aplicaciones científicas se utilizan estructuras de datos muy 
grandes (por ejemplo, una matriz de 10.000 × 10.000 elementos). Sin 
embargo, tal vez sólo haya que procesar unos pocos elementos de esas 
estructuras. Un ejemplo podría ser el de la figura. 
 
 
 
 
 
 
 
Aunque la matriz es muy grande, sólo se van a procesar los elementos 
marcados. Con dichos elementos puede formarse un vector, pero tenemos un 
pequeño problema. Hasta el momento, el paso (distancia de un elemento al 
 s = 3 
5 
14 
4 
1 8 
▪ 62 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
siguiente) de los vectores que hemos utilizado ha sido constante. Sin 
embargo, el vector que definimos en la figura no tendría un paso constante. 
Por tanto, no podríamos aplicar el mecanismo normal de direccionamiento 
para acceder a un elemento a partir del anterior: sumar una constante. En 
general, el problema es el siguiente: ¿cómo procesar vectorialmente 
vectores cuyo paso no es constante? Necesitamos un nuevo modo de 
acceso a memoria (un nuevo modo de direccionamiento) para poder leer o 
escribir dicho vector. 
El nuevo método de acceso se logra mediante el uso de vectores de 
índices. Un vector o registro de índices guarda las posiciones concretas de 
los elementos a los que queremos acceder. 
Una operación vectorial de este estilo se suele dividir en tres fases: 
 
1 Fase de agrupamiento (gather): se utiliza el registro de índices para 
leer los elementos que nos interesan —base + desplazamiento—, y se 
cargan en un registro vectorial. 
 
2 Fase de ejecución: se ejecuta la operación indicada. 
 
3. Fase de difusión (scatter): se llevan los resultados de la operación 
vectorial a memoria, a las posiciones correspondientes, utilizando 
nuevamente el registro de índices. 
 
Para efectuar las operaciones de agrupamiento y difusión y, en general, 
para trabajar con índices, se pueden utilizar instrucciones tales como (por 
ejemplo): 
 
LVI V1,A(V2) Lee de memoria los elementos A + V2(i), utilizando V2 
como registro de índices. 
 
SVI A(V2),V1 Escribe en memoria los elementos A + V2(i), utilizando V2 
como registro de índices. 
 
CVI V1,R1 Genera un vector de índices, con los valores 0, R1, 2R1, ..., 
(Lmax–1)R1. 
 
Por ejemplo, analicemos este bucle: 
 
do i = 0, M-1 
 A(i*i) = B(i*i) + 1 
enddo 
 
1.5 TÉCNICAS DE COMPILACIÓN PARA GENERAR CÓDIGO VECTORIAL ▪ 63 ▪ 
Tal como está, no se puede vectorizar por el procedimiento habitual, 
puesto que los pasos de los vectores A y B no son constantes (0, 1, 4, 9...). 
Sin embargo, tenemos la posibilidad de ejecutarlo vectorialmente así: 
 
MOVI VL,#M 
MOVI R1,#1 
CVI V4,R1 ; 0, 1, 2, 3... 
MULV V5,V4,V4 ; registro de índices: i*i 
 
 
LVI V1,B(V5) ; direccionamiento indexado 
ADDVI V2,V1,#1 
SVI A(V5),V2 
 
Para indicar los índices hemos utilizado el registro V5, en el que hemos 
cargado previamente los resultados de la función i*i. Después, hemos 
utilizado el modo de direccionamiento indexado (base + vector de índices) 
para acceder al vector. 
 
El modo de direccionamiento “indexado” puede utilizarse también, por 
ejemplo, para ejecutar el bucle del apartado anterior —if (B(i)>5) 
then A(i) = A(i) + 1— de la siguiente manera: 
 
MOVI VL,#N 
MOVI VS,#1 
 
MOVI F1,#5 
LV V1,B(R1) 
SGTVS V1,F1 ; generar máscara (VM) 
 
MOVI R2,#1 ; create vector index: 0, 1, 2... teniendo en cuenta VM 
CVI V2,R2 ; p.e.: VM = 10011101 → V2 = 03457 
POP R1,VM ; contar bits a 1 en el registro VM (5) 
MOV VL,R1 ; cargar el registro VL (número de elementos) 
CVM ; inicializar máscara 
 
LVI V3,A(V2) ; utilizar V2 como registro de índices, 
ADDVI V4,V3,#1 ; y procesar solamente VL elementos 
SVI A(V2),V4 
 
De este modo, en la última parte (LVI / SVI) no se leen y escriben todos 
los elementos (como se haría con un LV o SV normal), sino solamente los 
que se tienen que procesar. 
 
 
▪ 64 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
1.6 RESUMEN 
Un computador vectorial es una máquina específicamente diseñada para el 
procesamiento de vectores (o, visto de otra, manera para la ejecución de 
bucles “largos”), y está compuesta por dos secciones: la que procesa 
vectores y la que procesa escalares, tan importante como la primera (los 
programas reales incluirán ambos tipos de código, vectorial y escalar, por lo 
que es necesario que el computador ejecute código escalar eficientemente). 
El conjunto de instrucciones de estas máquinas incluye instrucciones 
vectoriales (LV, ADDV, SV...) que permiten la ejecución de una operación 
vectorial completa sobre todos los elementos de un vector con una sola 
instrucción. Las características arquitecturales básicas de un procesador 
vectorial son: el uso de registros vectoriales, el encadenamiento entre 
instrucciones, un gran ancho de banda con memoria (múltiples buses de 
acceso a memoria) y una memoria entrelazada en muchos módulos. Así, el 
modelo de ejecución lleva a que el tiempo de ejecución de los bucles pueda 
formularse como TV = ti + tv N (ti = tiempo de inicio; tv = tiempo necesario 
para procesar un elemento, 1 ciclo en el caso ideal), en lugar del modelo 
escalar tradicional, TE = te N. 
Las medidas básicas de rendimiento de un procesador vectorial ejecutando 
un determinado programa son R∞ (velocidad de cálculo con vectores de 
longitud infinita) y N1/2 (tamaño mínimo de los vectores para conseguir al 
menos la mitad de la velocidad máxima). Tal como ocurre con otros modelos 
de proceso, la velocidad pico (peak performance) de un procesador vectorial 
no es un parámetro adecuado para medir el rendimiento de un sistema 
vectorial. El que los vectores que se procesen no sean muy grandes, hace que 
el tiempo de inicio del cálculo vectorial (start-up) sea un parámetro muy 
importante a considerar. 
Sin embargo, hay varios factores que limitan el rendimiento. Por una 
parte, el hardware —el tamaño de los registros vectoriales, el número de 
buses a memoria, el número de unidades funcionales—. Pero el parámetro 
que más puede llegar a reducir el rendimiento de una máquina vectorial es el 
factor de vectorización,f: fracción de código que se ejecuta en modo 
vectorial. 
Por ello, no es posible olvidar el papel que un buen compilador debe 
realizar en este tipo de máquinas. El compilador vectorial es el responsable 
de generar código vectorial a partir de un código escalar estándar, y debe 
lograr el factor de vectorización más alto posible. En caso contrario, y tal 
1.6 RESUMEN ▪ 65 ▪ 
como indica la ley de Amdahl, el rendimiento final del sistema será muy 
bajo. Como siempre, para facilitar la tarea del compilador y mejorar su 
rendimiento, la ayuda de un usuario experto es siempre importante. Algunas 
de las técnicas de vectorización son ya clásicas y las aplican todos los 
compiladores. Esas estrategias se basan en el análisis de las dependencias 
entre instrucciones, y son comunes a los compiladores que intentan 
paralelizar el código para ser ejecutado en sistemas con más de un 
procesador. Por ello, las volveremos a analizar en un tema posterior. 
 
▪ Breve historia de los computadores vectoriales 
A lo largo de la (breve) historia de la computación, los computadores 
vectoriales han estado siempre a la cabeza de las máquinas más rápidas en 
cálculo científico, aunque la evolución de los sistemas multiprocesador ha 
relegado a estos procesadores a un segundo lugar. Pioneros en el uso de 
tecnologías avanzadas (ECL) y aportando soluciones arquitecturales 
novedosas, han marcado, hasta hoy en día, la referencia de velocidad de 
cómputo. 
Aunque el primer computador vectorial fue el CDC STAR-100 (1972), el 
computador que marcó la historia de este tipo de máquinas fue el Cray-1 
(1975), en la que se utilizaron por vez primera los registros vectoriales y el 
encadenamiento. Junto a ello, tomando en consideración los resultados de la 
ley de Amdahl, utilizaba un procesador escalar de gran velocidad (el más 
rápido del momento). En todo caso, y por limitaciones tecnológicas del 
momento, sólo disponía de una unidad vectorial, (es decir, sólo podía 
ejecutar una instrucción LV o SV a la vez). 
En 1981, la casa CDC pone en el mercado el CYBER 205, evolución 
natural del computador STAR: seguía manteniendo el modelo M/M, pero 
disponía de muchas unidades de memoria (de hecho, por lo menos se 
necesitan tres en un computador vectorial M/M). En ese computador se 
utilizaron por primera vez los vectores de índices para procesar matrices de 
baja densidad (sparse). 
La siguiente máquina de CRAY fue el Cray X-MP; una evolución natural 
del computador anterior (reloj más rápido, más buses a memoria, posibilidad 
de utilizar más de un procesador). Pronto aparece en el mercado el Cray-2: 4 
procesadores, 156 MB de memoria DRAM (palabras de 60 bits), reloj más 
rápido, pero latencias más altas (ciclos) en las unidades funcionales, sin 
encadenamiento, y un único bus; no era una gran alternativa, salvo por su 
gran memoria. 
▪ 66 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
En los 80 aparecen en el mercado los superminicomputadores, mucho más 
baratos que los anteriores. Entre ellos el C-1 y C-2 (2 procesadores) de 
Convex. El éxito de estas máquinas, además de en su precio, hay que 
buscarlo en su compilador, de una gran eficiencia. 
Los computadores Japoneses entran en escena. Los computadores VP100 
y VP200 de Fujitsu se comercializan en 1983, y un poco después aparecen 
los Hitachi S810 y NEC SX/2. En general, estas máquinas japonesas podían 
lograr velocidades pico muy altas, pero los tiempos de inicio de las 
operaciones vectoriales (start-up) eran muy altos, lo que los hacía muy 
sensibles al procesamiento de vectores no muy largos, logrando resultados 
en muchos casos peores que los del X-MP. 
En 1988 aparece el Cray-Y-MP, una evolución del X-MP (8 procesadores, 
reloj más rápido). La casa Cray continua adelante y ofrece el C90 —16 
procesadores a 240 MHz (y 15 millones de dólares)— y, más tarde, el T90. 
Comercializa también el J90, una versión CMOS, más barata (1 millón $). 
Más tarde, la casa Cray comercializó el computador vectorial Cray Inc. 
SV1(ex), sucesor del J90 y del T90, en el que se utiliza tecnología CMOS (y 
se abandona definitivamente la rápida y cara ECL, tal como hicieron en su 
día Fujitsu y NEC). Se trata de un multiprocesador de memoria compartida 
que, en su configuración máxima, utiliza 128 procesadores vectoriales, a 450 
MHz y 1,8 Gflop/s. Otra característica a destacar es el uso de una cache 
común de 256 kB para escalares y vectores. En anteriores diseños de Cray 
no se utilizaba memoria cache, pero en este último la velocidad del sistema 
de memoria no es suficiente para mantener ocupado el procesador (efecto del 
tradicional gap entre la velocidad del procesador y la de la memoria). 
Por su parte, Fujitsu ofreció el computador VPP5000, evolución natural 
del VPP700: reloj más rápido (300 MHz) y 16 vector pipes de tipo multiply-
and-add. En teoría, por tanto, cada procesador es capaz de lograr 9,6 
Gflop/s. El procesador escalar va a 1,2 Gflop/s. En su configuración mayor, 
se trata de un multicomputador de memoria distribuida de 128 procesadores, 
en el que la comunicación punto a punto se efectúa a 1,6 GB/s, utilizando 
como red de comunicación un full distributed crossbar. 
Finalmente, otra máquina japonesa más: el NEC SX-5/16A, un 
multiprocesador vectorial de memoria distribuida. Sus características 
principales son: reloj a 313 MHz, 16 unidades vectoriales, 10 Gflop/s por 
procesador. La versión SX-6 de ese procesador fue la base del 
supercomputador Earth Simulator. 
1.6 RESUMEN ▪ 67 ▪ 
Todas las máquinas citadas han sido siempre las más rápidas del 
momento, pero también, con diferencia, las más caras. La evolución de los 
microprocesadores en los últimos años, junto con el uso del paralelismo, ha 
ido arrinconando a este tipo de arquitecturas, con lo que, en un futuro 
cercano, parece que jugarán un papel cada vez menor en el campo del 
cálculo científico. Para ello, habrá que aprender a programar y utilizar los 
sistemas de muchos procesadores de manera eficiente, para aprovechar su 
gran potencial de cálculo. En todo caso, es habitual que los procesadores 
(super)escalares actuales dispongan de instrucciones de tipo vectorial 
(SIMD) que, por ejemplo, dividen los 64 bits de una palabra en 8 palabras de 
8 bits que son tratadas como un vector corto, y con las que se realizan 
operaciones tipo producto/suma encadenadas. 
Hoy en día, los procesadores vectoriales aparecen como nodos 
especializados de un sistema paralelo más general. En ese tipo de sistemas, 
MPP (massive parallel processors) hay que buscar el futuro del cálculo 
paralelo: miles de procesadores colaboran en la resolución de un problema y 
se comunican entre ellos mediante un red de comunicación de gran 
velocidad. En dicha red, algunos procesadores están especializados en 
determinado tipo de cálculo, por ejemplo, cálculo vectorial. 
Siempre es posible, en todo caso, encontrarse con sorpresas en la 
evolución de los computadores. En el top500 de junio de 2002 (lista de las 
500 máquinas más rápidas del mundo, que se publica dos veces al año), se 
produjo un cambio significativo. En contra de la línea seguida en los últimos 
años, el número 1 de la lista fue un (multi)computador vectorial: Earth 
Simulator. Se trataba de un computador japonés de propósito específico con 
5.120 procesadores vectoriales. Utilizaba chips NEC SX-6, que contienen 
cada uno 8 procesadores vectoriales. Lograba una velocidad de Rmax = 36 
Tflop/s (el segundo en dicha lista, junio 2002, el ASCI White, alcanzaba 7,2 
TF/s, utilizando 8.192 procesadores). En la lista citada (junio 2002) había 41 
computadores vectoriales. 
 
En 2009 disponemos de una nueva versión de dicha máquina (1.280 
procesadores NEC SX-9, de 350 millones de transistores, a 3,2 GHz) que ha 
logrado una velocidad de cálculo de 122,4 TF/s. Cada CPU dispone de una 
unidad superescalar (de 4 vías) y una unidad vectorial con las siguientes 
características: 72 registros vectoriales de 256 elementos y 8 conjuntos o 
pipes de unidadesfuncionales vectoriales (+, ×, /, lógicas, máscaras y 
LV/SV). Cada chip puede alcanzar los 102,4 GF/s. 
▪ 68 ▪ Capítulo 1: COMPUTADORES VECTORIALES 
 
 
 
El nuevo Earth Simulator es el número 22 de la lista top500 de junio de 
2009, pero es ya la única máquina vectorial de la lista; parece, por tanto, que 
la arquitectura vectorial tiende a desaparecer. 
 
En el último capítulo haremos un repaso de la situación de la lista top500 
y de las principales máquinas, arquitecturas y tendencias que en ella 
aparecen. 
 
▪ 2 ▪ 
 
 
Computadores Paralelos 
(conceptos básicos) 
 
 
 
 
 
 
 
2.1 INTRODUCCIÓN 
Aunque los procesadores son cada vez más rápidos, existen numerosas 
aplicaciones para las que la velocidad de cálculo de un único procesador 
resulta insuficiente. La alternativa adecuada para esas aplicaciones es el uso 
de paralelismo. Con el término paralelismo se indica que la ejecución de un 
determinado programa se reparte entre muchos procesadores, que trabajan 
simultáneamente. 
Pueden utilizarse diferentes niveles de paralelismo. Por ejemplo, se 
explota el paralelismo a nivel de instrucción (ILP) cuando se segmenta la 
ejecución de las instrucciones de un programa: en un momento dado, se 
están ejecutando muchas instrucciones a la vez, pero en fases de ejecución 
diferentes. También puede explotarse el paralelismo en los datos. El ejemplo 
con más éxito de esa alternativa son los computadores vectoriales que 
acabamos de analizar. En todos esos casos (y en otros similares, como 
VLIW), sólo existe un contador de programa o PC, es decir sólo se ejecuta 
▪ 70 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos) 
un programa bajo una única unidad de control. En los próximos capítulos 
vamos a estudiar el paralelismo a nivel de programa, es decir, vamos a 
analizar cómo repartir la ejecución de un programa entre P procesadores. Si 
fabricar réplicas de un procesador es un proceso relativamente sencillo y 
barato, ¿por qué no utilizar P procesadores para intentar ejecutar un 
programa P veces más rápido? 
Recordemos un momento la clasificación de Flynn de los computadores, 
que toma en cuenta el número de flujos de datos y de instrucciones: 
▪ SISD: un solo flujo de datos y un solo flujo de instrucciones. Se trata 
del modelo de un solo procesador (superescalar, por ejemplo). El 
paralelismo se obtiene en el uso simultáneo de unidades funcionales 
debido a la segmentación de la ejecución de las instrucciones. 
▪ SIMD: un solo flujo de instrucciones (un contador de programa), pero 
muchos flujos de datos. En función del uso de la memoria, pueden 
diferenciarse dos familias: los procesadores vectoriales (memoria 
compartida) y los procesadores en array de memoria distribuida. 
▪ MIMD: múltiples flujos de datos y de instrucciones. Se trata del 
verdadero modelo de paralelismo, en el que existen muchos 
programas en ejecución simultanea. Éste es el tipo de máquina que 
vamos a analizar a partir de ahora. 
 
Antes de ello, una pequeña precisión acerca del uso de P procesadores, 
puesto que podemos tener diferentes alternativas, en función del objetivo que 
busquemos: 
▪ Redes de computadores (LAN, WAN...). P usuarios ejecutan cada uno 
de ellos un programa diferente, independientemente (tal vez, de vez en 
cuando, se produzca alguna transmisión de datos entre los usuarios). 
Cada programa se ejecuta según el modelo de un único procesador. 
▪ Tolerancia a fallos. En función de la aplicación, existen diferentes 
maneras de hacer frente a los fallos del sistema. Por ejemplo, se repite 
la ejecución del mismo programa en P procesadores, para obtener un 
alto nivel de fiabilidad de los resultados (por ejemplo, en situaciones 
especiales en las que no podemos permitirnos un error), o para 
disponer de una máquina cuando falla otra (high reliability, en un 
banco, por ejemplo) En otros casos, un computador ofrece un 
determinado servicio y un segundo computador está a la espera del 
posible fallo del primero, y cuando lo detecta toma su función para 
que el servicio ofertado esté siempre disponible (high availability). 
2.2 COMPUTADORES DM-SIMD ▪ 71 ▪ 
▪ Se ejecuta el mismo programa, repetido en todos los procesadores, 
pero con datos diferentes; por ejemplo, para hacer múltiples 
simulaciones independientes de un proceso físico en menor tiempo 
(mejora del throughput). O, en los servidores, para poder atender a 
múltiples peticiones simultáneas (por ejemplo, en una base de datos). 
• Para ejecutar un programa P veces más rápido (high 
performance). Éste es el tipo de aplicación que nos interesa. 
Comparado con los anteriores casos, la diferencia fundamental va a 
estar en la comunicación entre procesos, que va a ser mucho más 
intensiva y que habrá que efectuar en tiempos muy breves 
(microsegundos). Esta comunicación es una parte de la ejecución y se 
produce como consecuencia de ejecutar en paralelo. Existen diferentes 
arquitecturas de este tipo, en función del número de procesadores, el 
nivel de acoplamiento entre ellos (frecuencia de la comunicación), 
capacidad y complejidad de los procesadores, mecanismos de 
sincronización y control, tamaño de las tareas, etc. 
En los próximos capítulos vamos a analizar las principales características 
y problemas de este nuevo modelo de ejecución. Pero antes de ello, vamos a 
definir los principales conceptos y terminología de esta área. 
 
2.2 COMPUTADORES DM-SIMD 
Acabamos de analizar los computadores vectoriales, máquinas SIMD de 
memoria compartida. Aunque no son nuestro objetivo, vamos a hacer un 
breve resumen de las características principales del otro tipo de arquitecturas 
SIMD, las de memoria distribuida (DM = distributed memory) o 
procesadores en array. 
Como ya hemos comentado, los computadores SIMD explotan el 
paralelismo de datos: con una única instrucción (la misma en todos los 
procesadores en el caso de los arrays) se procesan múltiples datos. 
 
 
 
 
 
 
 
Procesador 
de control 
 
Computador 
front-end 
Pr + M + I/O 
Red de comunicación 
Array de cálculo 
▪ 72 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos) 
Las características principales son las siguientes: 
 
- Procesadores: en general, se utilizan muchos procesadores muy 
sencillos, por ejemplo, 1.024 procesadores de 1 bit. Así pues, 
procesadores baratos, pero no muy rápidos. En el caso de los 
procesadores serie, de 1 bit, se procesan datos de cualquier tamaño, 
siendo la latencia proporcional al tamaño de los mismos. 
 
- Control: el control es centralizado. Todos los procesadores ejecutan la 
misma instrucción en el mismo momento (lock-step), sobre datos 
diferentes. Si es necesario, la ejecución puede controlarse mediante 
máscaras, que indican en qué procesadores sí y en cuáles no se debe 
ejecutar la instrucción actual. 
 Un procesador especial de control se encarga de repartir las 
instrucciones a los procesadores y de comunicarse con el computador 
central front-end, desde el que se controla todo el sistema. Como en el 
caso de los procesadores vectoriales, el código que no se pueda 
ejecutar en el array se ejecutará en serie en el procesador central (o en 
el de control). 
 Normalmente, las operaciones de entrada/salida se realizan en los 
procesadores del array, lo que resulta muy adecuado para procesar 
datos de manera intensiva. 
 
- Estructura: los procesadores forman una matriz o array, de 2 o 3 
dimensiones. Una red especial de comunicación facilita la 
comunicación entre los procesadores; las redes más habituales son las 
mallas, los toros, etc. En general, y de cara a mejorar la eficiencia del 
sistema, la red se suele dividir en diferentes planos o subredes: para 
datos, para control, etc. 
 
- Aplicaciones: este tipo de estructura se adecua muy bien a un 
determinado tipo de aplicaciones; por ejemplo, procesamiento de 
señales y de imágenes, o cierto tipo de simulaciones (Montecarlo...). 
Aunque el espacio de memoria sea común, la eficiencia del sistema es 
mucho mayor silas comunicaciones son locales (con los vecinos), que 
es lo que ocurre en las aplicaciones que hemos citado. 
 La regularidad de las estructuras de datos que se procesan y el tipo de 
operaciones que se ejecutan hacen que los accesos a memoria se 
realicen de acuerdo a patrones conocidos, en muchos casos en forma 
de “permutaciones". 
2.3 COMPUTADORES MIMD ▪ 73 ▪ 
ILLIAC IV, Solomon, CM1, BSP, DAP, Quadrics Apemille, procesadores 
sistólicos... son algunas de las máquinas más conocidas que han utilizado 
este tipo de arquitectura. Aunque han tenido su importancia, los 
computadores SIMD únicamente han encontrado un hueco en el tipo de 
aplicaciones citadas, y hoy no tienen presencia alguna en el mercado. 
Los sistemas paralelos actuales son de tipo MIMD; veamos, por tanto, las 
características principales de estos sistemas. 
 
2.3 COMPUTADORES MIMD 
Como ya hemos comentado, en un sistema MIMD las aplicaciones se 
reparten en múltiples procesos que se ejecutan en diferentes procesadores. 
Desde el punto de vista de la arquitectura del sistema, la primera cuestión a 
aclarar sería: ¿cómo se estructuran los P procesadores en un sistema único? 
La respuesta puede ser muy amplia, pero pueden identificarse dos grandes 
grupos de arquitecturas, de acuerdo al uso de la memoria: los sistemas de 
memoria compartida y los de memoria distribuida o privada. 
2.3.1 Memoria compartida (shared memory) 
En los sistemas paralelos de memoria compartida, todos los procesadores 
comparten la memoria global del sistema, es decir, todos los procesadores 
utilizan el mismo espacio de direccionamiento. 
De esta manera, la comunicación entre procesos es relativamente sencilla, 
utilizando para ello variables compartidas en la memoria común. Para pasar 
un dato de un proceso a otro, basta con dejar el dato en una determinada 
posición de memoria, donde lo leerá el proceso destino. 
 
 
 
 
 
 
 
 
P0 
 
M0 
 
Mm–1 
 
P1 
 
Pp–1 
Procesadores (+ MC) 
Red de comunicación 
Memoria principal 
 
sistema 
E/S 
▪ 74 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos) 
Para conectar los procesadores y la memoria se utiliza una red de 
comunicación. La red más sencilla es el bus; se conoce perfectamente su 
funcionamiento y no es difícil de controlar. Sin embargo, tendremos un 
problema nuevo: si se conectan muchos procesadores al bus, es posible que 
éstos lleguen a saturarlo, y que, por tanto, los tiempos de acceso a memoria 
sean altos. No hay que olvidar que el bus es una red centralizada que se 
comparte en el tiempo, que no admite dos operaciones a la vez. También 
pueden utilizarse otro tipo de redes, que analizaremos más adelante. Para 
simplificar, vamos a suponer que la red de comunicación es un bus. 
A este tipo de arquitectura se le conoce habitualmente con el nombre de 
multiprocesador, y también como SMP (symmetric multiprocessor), UMA 
(uniform memory access) o sistemas paralelos de alto grado de 
acoplamiento. Dada la red de comunicación, un bus, el número de 
procesadores de un sistema SMP es relativamente bajo, entre 2 y 32, por lo 
que el paralelismo que se puede conseguir es reducido. 
2.3.2 Memoria privada o distribuida (distributed memory) 
En este segundo modelo, como puede observarse en la figura siguiente, 
cada procesador dispone de su propia memoria privada. El espacio de 
direcciones no es común: todas las direcciones son locales y hacen referencia 
a la memoria propia del procesador. Por ello, la comunicación entre procesos 
no puede hacerse, como en el caso anterior, mediante posiciones comunes de 
memoria. Así, la comunicación se realiza mediante paso de mensajes, 
utilizando para ello la red de comunicación. Si Pi debe enviar datos a Pj, 
formará con ellos un mensaje y lo enviará a la red; los controladores de la 
red se encargarán de ir retransmitiendo el mensaje hasta que llegue a su 
destino. 
 
 
 
 
 
 
 
 
Nodos: 
Procesador (+ MC) + Memoria principal + E/S + Contr. comunic. 
Red de comunicación 
 
P0 
 
M 
E/S 
K 
 
Pp–1 
 
M 
E/S 
K 
2.3 COMPUTADORES MIMD ▪ 75 ▪ 
El objetivo de este modelo es conseguir paralelismo “masivo”, es decir, 
poder utilizar un número grande de procesadores. Por ello, no se utiliza un 
bus como de red de comunicación, sino redes tales como mallas y toros de 2 
y 3 dimensiones, hipercubos, árboles, etc., que analizaremos más adelante. 
A este tipo de arquitectura se le conoce como multicomputador (o 
también como sistema débilmente acoplado, MPP o Massively Parallel 
Processors...). 
2.3.3 Memoria lógicamente compartida pero 
físicamente distribuida (distributed shared memory) 
Existe una tercera alternativa, que corresponde a una mezcla de las dos 
anteriores. Cuando el espacio de memoria es común, la programación de 
aplicaciones suele resultar más sencilla, pero la memoria se convierte en un 
cuello de botella: se producen grandes atascos de tráfico, provocados por los 
procesadores del sistema, que tienen que acceder a la memoria común a 
través de una red tipo bus. Cuando la memoria es privada en cada 
procesador, este problema desaparece, pero la comunicación entre 
procesadores es más compleja y también lo son los modelos de 
programación. 
Un análisis sencillo de los programas muestra que los procesadores no 
hacen un uso homogéneo de la memoria, es decir, no acceden con la misma 
probabilidad a cualquier posición de memoria; ello permite pensar en una 
alternativa mixta: compartir el espacio de memoria pero distribuirla 
físicamente entre los procesadores. La estructura que corresponde a este 
modelo mixto es la de la figura anterior, pero todos los procesadores tienen 
acceso a todos los bloques de memoria. 
Tiene que quedar claro que estamos organizando la memoria principal de 
manera jerárquica: los accesos locales serán rápidos, pero los externos 
serán mucho más lentos, puesto que hay que salir a la red de comunicación. 
Esperamos, en todo caso, que el acceso a la memoria local sea mucho más 
frecuente que a la memoria “remota”, y que la red de comunicación se utilice 
principalmente para la comunicación entre procesos. 
Esta última estructura es la que está obteniendo el mayor éxito y 
desarrollo en la actualidad, y habitualmente se conoce como NUMA (Non 
Uniform Memory Access) o también como MPP. 
▪ 76 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos) 
2.3.4 Clusters, constellations... y otros 
Las arquitecturas que hemos citado son las principales, y hacen referencia 
al uso de memoria por parte de los procesadores. Es muy habitual que 
encontremos todo tipo de mezclas entre ellas. Por ejemplo, en la mayoría de 
los supercomputadores actuales los nodos que forman el sistema, y que se 
conectan mediante una red de comunicación, no son simples procesadores, 
sino pequeños sistemas paralelos SMP con 4-8 procesadores conectados en 
un bus. Así, dentro de cada nodo la memoria es compartida, pero la de otros 
nodos es privada. 
Por otra parte, y tratando de reducir el elevado coste de los 
supercomputadores de diseño específico, han aparecido en el mercado con 
fuerza los sistemas formados por hardware sencillo y barato: computadores 
de propósito general conectados entre sí mediante redes más o menos 
sencillas derivadas de las tecnologías de las redes de computadores. En 
general, a este tipo de sistemas se les denomina clusters. Así pues, para 
formar un cluster se necesita un conjunto de nodos de cómputo y una red de 
comunicación (junto con el software de gestión y programación adecuado). 
La eficiencia del cluster ejecutando código paralelo será función de ambos, 
nodos y red. En el caso más simple, los nodos son simples PCs y la red de 
comunicación es (Gigabit) Ethernet. Ese tipo de sistema se conoce como 
Beowulf; es la opción más barata, pero también la de menores prestaciones, 
aunque ofrece buenos resultados en aquellos casos en los que la 
comunicación entre procesos no es relevante. 
Para conseguir clusters más eficientes, pueden usarse pequeños sistemasSMP como nodos de cálculo y redes de comunicación más sofisticadas 
(Myrinet, Infiniband, Quadrics…); cuando el número de procesadores de 
cada nodo del cluster es mucho mayor que el número de nodos, el sistema se 
conoce también con el nombre de constellation. 
Todos los fabricantes ofrecen hoy en día diversos tipos de clusters en sus 
catálogos (custom clusters) como una alternativa interesante para conseguir 
máquinas de alto rendimiento a un coste “razonable”. Además, es 
relativamente sencillo montar un cluster de no muy alto rendimiento 
conectando unos cuantos PC entre sí (commodity clusters). 
Sea cual sea la arquitectura del sistema paralelo, en todos ellos es 
necesario resolver una serie de problemas comunes para poder lograr un 
buen rendimiento. Analicemos brevemente los principales problemas a los 
que hay que hacer frente. 
2.4 ALGUNOS PROBLEMAS ▪ 77 ▪ 
2.4 ALGUNOS PROBLEMAS 
En cualquiera de sus estructuras, un computador MIMD presenta 
numerosos problemas nuevos para resolver. Por ejemplo: 
 
▪ Gestión del sistema: la máquina construida a partir de múltiples 
procesadores o, incluso, computadores autónomos, debe aparecer al 
usuario como un único sistema integrado. Van a ser necesarios para 
ello nuevos sistemas operativos específicos, mecanismos adecuados 
para la gestión distribuida de las tareas, nuevas herramientas de 
monitorización, controles de seguridad avanzados, etc. Son todas ellas 
cuestiones muy importantes, pero no las trataremos en este texto. 
 
▪ Reparto de tareas. ¿Sabemos cómo repartir un programa secuencial 
entre P procesadores? En algunos casos será muy sencillo; por 
ejemplo, es muy fácil repartir entre N procesadores la ejecución del 
bucle do i = 1,N {A(i) = A(i) + 1}; cada uno ejecuta una 
iteración del bucle, cualquiera de ellas, ya que todas las iteraciones 
son independientes y por tanto da igual cómo se haga. Pero en los 
casos más generales puede que no sea sencillo sacar a la luz el 
paralelismo inherente a un determinado algoritmo. De hecho, en 
muchos casos va a ser necesario desarrollar nuevos algoritmos para 
resolver viejos problemas, que saquen partido de las posibilidades de 
la máquina paralela. En general, la programación paralela es más 
compleja que la programación secuencial o serie. 
 Junto a ello, es necesario mantener cierto equilibrio en el reparto de 
carga de trabajo a los procesadores (load balancing). Si repartimos la 
carga %80 - %20 entre dos procesadores, el sistema global no será en 
modo alguno dos veces más rápido, ya que la tarea más larga será la 
que marque el tiempo final de ejecución. El reparto de carga puede ser 
estático —en tiempo de compilación— o dinámico —en tiempo de 
ejecución—. El primero es más sencillo y no añade sobrecargas a la 
ejecución del programa, pero es más difícil mantener el equilibrio de 
la carga de trabajo. El segundo es más costoso en tiempo de ejecución, 
pero permite repartos más equilibrados. 
 
▪ Coherencia de los datos. Cuando se utilizan variables compartidas se 
cargan copias de dichas variables en las caches de los procesadores. 
Cuando se modifica una de dichas copias, ¿cómo se enteran del nuevo 
valor de la variable el resto de procesos? es decir, ¿cómo se mantienen 
▪ 78 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos) 
"coherentes" los datos compartidos? Como veremos en los próximos 
capítulos, la solución depende de la arquitectura del sistema. 
 
▪ Comunicación. Cuando hablamos de paralelismo, la comunicación 
entre procesos es el tema principal. Y junto a ello, la red de 
comunicación (sobre todo en los sistemas DSM o MPP). En un 
sistema paralelo, el tiempo de ejecución de un programa puede 
modelarse como: 
 
 Tp = Tej + Tcom 
 
 donde Tej representa el tiempo de ejecución real y Tcom el de 
comunicación. El tiempo de ejecución se reduce (en teoría) con el 
número de procesadores, pero el de comunicación en cambio, crece. 
La siguiente figura muestra una simplificación de ese compartimiento. 
 
 
 
 
 
 
 
 Como se observa en la figura, no siempre es una buena solución 
utilizar un número elevado de procesadores, ya que las necesidades de 
comunicación pueden echar por tierra cualquier otra ventaja. Es 
necesario por ello encontrar un punto de equilibrio. 
 Un tipo especial de comunicación es la sincronización. Un grupo de 
procesadores se sincroniza, por ejemplo, para esperar a que todos 
terminen una tarea antes de comenzar con la siguiente. Los procesos 
de sincronización pueden generar mucho tráfico en la red y momentos 
de gran congestión en el acceso a variables comunes. Analizaremos 
este problema un poco más adelante. 
 Considerando el reparto de tareas y la comunicación, suelen 
distinguirse diferentes tipos o niveles de paralelismo: 
 
• paralelismo de grano fino (fine grain): las tareas que se reparten 
entre los procesadores son "pequeñas", y la comunicación entre 
ellas es muy frecuente, aunque no se intercambian mucha 
información. 
Núm. procesadores 
Tej 
Tcom 
Tp 
2.5 RENDIMIENTO DEL SISTEMA PARALELO (leyes de Amdahl y Gustafson) ▪ 79 ▪ 
• paralelismo de grano grueso (coarse grain): las tareas que se 
reparten entre los procesadores son "grandes", y sólo se comunican 
entre ellas de vez en cuando, aunque en esos casos se intercambia 
gran cantidad de información. 
 
2.5 RENDIMIENTO DEL SISTEMA PARALELO 
(leyes de Amdahl y Gustafson) 
El coste de los sistemas paralelos es elevado, y por ello nuestro objetivo 
debe ser conseguir ir P veces más rápido cuando se utilizan P procesadores. 
Para comparar sistemas de un solo procesador y de P procesadores suelen 
utilizarse dos parámetros: el factor de aceleración (speed-up) y la eficiencia 
(efficiency). 
El factor de aceleración mide cuántas veces más rápido se ha ejecutado un 
determinado programa, es decir: 
 
fa = Ts / Tp 
 
donde Ts es el tiempo de ejecución en serie y Tp en paralelo. 
Por su parte, la eficiencia se define como: 
 
efic = fa / P (habitualmente en %) 
 
es decir, el tanto por ciento que se consigue del máximo factor de 
aceleración posible. 
En el mejor de los casos, tendremos que Tp = Ts / P; es decir, que el 
programa se ejecuta P veces más rápido usando P procesadores: 
 
fa = Ts / (Ts / P) = P 
efic = fa / P = 1 
 
Se trata, en todo caso, de la situación ideal que, debido a múltiples 
problemas —reparto no equilibrado de la carga, comunicación, 
sincronización...— es difícil de lograr13. En todo caso, aunque no logremos 
 
13 En algunos casos, pueden conseguirse factores de aceleración superlineales, es decir, mayores que P. 
En general, son debidos a otros factores, ya que, además de P procesadores, el sistema paralelo 
dispone de más memoria, más capacidad de entrada/salida, etc. Tal vez los datos/programas que no 
cabían en la memoria de un procesador, sí quepan ahora en todo el sistema, con lo que, como 
sabemos, se ahorrará tiempo. 
▪ 80 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos) 
que el factor de aceleración sea P, deberíamos conseguir que creciera 
linealmente con el número de procesadores (o, lo que es equivalente, que la 
eficiencia fuera constante, independiente de P): si se duplica el número de 
procesadores, que se duplique también el factor de aceleración. 
No todos los programas presentan esas características, ya que no podemos 
olvidarnos de la ley de Amdahl. Tal como ha ocurrido en el caso de los 
computadores vectoriales, los programas más habituales no pueden 
ejecutarse completamente en paralelo: siempre queda una parte del código 
que hay que ejecutar en serie (o en un número reducido de procesadores). 
Como ejemplo, supongamos que una fracción del código, f, puede 
ejecutarse en P procesadores, mientras que el resto, 1–f, debe ejecutarse en 
un único procesador. En ese caso, el tiempo de ejecución debe escribirse así: 
 
Tsp = f × Tp + (1–f) × Ts (en general, ∑
=
=
P
i
s
isp i
T
fT
1
) 
Si no consideramos el tiempode comunicación, y tomamos el mejor caso, 
Tp = Ts / P, entonces el speed-up o factor de aceleración será: 
 
fa = Ts / Tp = P / [ P (1–f) + f ] 
 
Por ejemplo, si P = 1.024 y f = 0,98, entonces fa = 47,7, muy lejos del 
hipotético 1.024. Como se muestra en la siguiente figura, el factor de 
aceleración se satura, con una asíntota de valor 1 / (1–f), y queda muy lejos 
del comportamiento lineal. 
 
 
 
 
 
 
 
 
 
 
 
 
2.5 RENDIMIENTO DEL SISTEMA PARALELO (leyes de Amdahl y Gustafson) ▪ 81 ▪ 
De acuerdo a la ley de Amdahl, el efecto de la parte de código que haya 
de ejecutarse en serie es muy grande cuando el número de procesadores es 
grande. Si se cumple en la realidad lo que pronostica dicha ley, va a ser muy 
difícil conseguir factores de aceleración (speed-up) altos. Como acabamos 
de ver en el ejemplo anterior, basta que un 2% del código tenga que 
ejecutarse en serie para que el factor de aceleración se reduzca de 1.024 a 47 
(a menos del 5%). 
Sin embargo, se comprueba que en muchos casos se consiguen 
aceleraciones reales mucho mayores que las pronosticadas. ¿Dónde está el 
error? Cuando hemos planteado la ley de Amdahl hemos considerado la 
siguiente hipótesis: se utilizan P procesadores para hacer que un 
determinado algoritmo se ejecute más rápido. Pero en realidad, muchas 
veces lo que ocurre es que se utilizan P procesadores para ejecutar un 
problema de tamaño más grande en el mismo tiempo. Por ejemplo, se 
ejecutan más ciclos de simulación o se hacen análisis considerando una red 
de más puntos, etc. En resumen, se mantiene el tiempo de ejecución, no las 
dimensiones del problema. 
Se ha podido comprobar experimentalmente que cuando se hace crecer el 
tamaño del problema (por ejemplo, se usan matrices más grandes) no suele 
crecer el tamaño del código que se debe ejecutar en serie (al menos no en la 
misma proporción). Esto es equivalente a decir que al crecer el tamaño del 
problema crece también f (no es un valor constante). Si es así, para calcular 
el factor de aceleración deberíamos comparar estas dos situaciones: 
 
 
 
 
 
 
 
 
 
 
 
 
 tamaño del problema constante tiempo de ejecución constante 
(1–f) Ts f Ts 
 f Ts × P (1–f) Ts 
(1–f) Ts f Ts (1–f) Ts f Ts / P 
en paralelo 
problema de mayor tamaño 
1 procesador 
P procesadores 
trozo que hay que 
ejecutar en serie 
trozo que se puede 
ejecutar en paralelo 
en paralelo 
▪ 82 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos) 
Por tanto, cuando el tiempo de ejecución se mantiene constante: 
 
Ts = (1–f) Ts + f Ts P 
Tp = (1–f) Ts + f Ts = Ts → fa = Ts / Tp = (1–f) + f P 
 
La expresión que acabamos de obtener para el factor de aceleración se 
conoce como ley de Gustafson, y es lineal con P, lo que asegura que se 
pueden conseguir factores de aceleración elevados. Por ejemplo, como en el 
caso anterior, si P = 1.024 y f = 0,98, el factor de aceleración que se 
consigue resulta ser fa = 1.003,5. 
Como comparación, la siguiente figura muestra la evolución con P del 
factor de aceleración en su doble versión, para el caso f = 0,9. 
 
 
 
 
 
 
 
 
 
 
 
En la realidad, y para un programa dado, el factor de aceleración concreto 
estará en algún punto entre esos dos extremos. 
 
En los siguientes capítulos vamos a analizar algunos de los problemas que 
hay que resolver para poder utilizar de manera eficiente un sistema paralelo 
MIMD; entre ellos, la coherencia de los datos (tanto en sistemas SMP como 
DSM), la sincronización, el modelo de consistencia, la red de comunicación, 
y las estrategias de paralelización de bucles. En el último capítulo 
presentaremos brevemente el mercado de sistemas paralelos de alta 
velocidad, algunas de las implementaciones de más éxito, así como una 
pequeña introducción a las herramientas más utilizadas para programar 
aplicaciones paralelas (OpenMP y MPI). 
 
▪ 3 ▪ 
 
 
Coherencia de los Datos 
en los Computadores SMP 
 
 
 
 
 
 
 
3.1 PRESENTACIÓN DEL PROBLEMA Y REVISIÓN 
DE CONCEPTOS 
La velocidad de ejecución de programas que puede alcanzar un 
procesador está íntimamente ligada a la estructura y funcionamiento del 
sistema de memoria. Desgraciadamente, la velocidad de respuesta de la 
memoria principal es significativamente menor que la del procesador, y la 
diferencia es cada vez mayor. Por eso, para poder obtener datos e 
instrucciones en el menor tiempo posible, la memoria de un computador se 
organiza en forma jerárquica: registros, cache interna, cache externa, 
memoria principal, (discos...). Cada uno de los niveles es un subconjunto del 
nivel superior. Los registros son los más rápidos y cercanos al procesador, 
pero su capacidad es pequeña (por ejemplo, 128 registros de 64 bits); en el 
extremo opuesto tenemos la memoria principal, de alta capacidad (ya con 2 o 
más GB) pero de tiempo de respuesta mucho mayor (p.e., 50 ns). 
▪ 84 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
El funcionamiento de la jerarquía de memoria se basa en el hecho de que 
el acceso a datos e instrucciones no es aleatorio. Así, podemos utilizar esa 
propiedad de los programas para reducir la latencia media de los accesos a 
memoria, si vamos llevando a la memoria más rápida los datos que 
prevemos va a necesitar el procesador. Ésa es por tanto la función de la 
memoria cache: tener preparados los datos (instrucciones) que "pronto" o 
con más "frecuencia" utiliza el procesador, ya que el tiempo de respuesta de 
la cache es del orden de 5 a 10 veces menor que el de la memoria principal. 
Así pues, se copian en la cache de datos del procesador algunos de los 
bloques14 de datos de la memoria principal; es decir, el procesador va a 
trabajar con copias de los datos. 
El hecho de trabajar con copias presenta un nuevo problema en los 
multiprocesadores de memoria compartida: hay que asegurar que las 
posibles copias de los datos que estén en las caches del sistema sean todas 
iguales, es decir, que sean coherentes. De no asegurarse la coherencia de los 
datos, los procesos no podrán utilizar variables compartidas, ya que nunca 
estarán seguros de sus valores reales. 
3.1.1 Coherencia de los datos en los sistemas de un 
solo procesador 
El problema de coherencia no se presenta exclusivamente en los 
multiprocesadores, sino que también aparece en los sistemas con un solo 
procesador, ya que también en ese caso se utilizan copias de los datos: una 
en la memoria cache y otra en la memoria principal15. El problema sin 
embargo no es complicado de resolver, ya que ambas copias están bajo 
control del único procesador existente. 
Cuando se quiere modificar una palabra de la cache, ¿qué hay que hacer 
con las dos copias que existen de dicho bloque? Ya conocemos las dos 
políticas de escritura habituales: 
 
▪ Write-through (WT): se actualizan ambas copias, la de la cache y la de 
la memoria principal, con lo que el sistema se mantiene siempre 
 
14 El bloque es la unidad de transferencia entre la memoria cache y la memoria principal. Se trata de un 
conjunto de palabras consecutivas de memoria (por ejemplo, bloques de 64 bytes: 16 palabras de 32 
bits, u 8 palabras de 64 bits), estando el tamaño del bloque directamente relacionado con el nivel de 
entrelazado de la memoria. El término inglés para bloque suele ser line. 
15 En los procesadores actuales la memoria cache está dividida en dos o tres niveles, por lo que el 
número de copias de un determinado bloque de datos puede ser mayor. 
3.1 PRESENTACIÓN DEL PROBLEMA Y REVISIÓN DE CONCEPTOS ▪ 85 ▪ 
 
coherente. Ello implica que todas las escrituras se efectúan también en 
memoria principal, lo que requiere más tiempo. 
▪ Write-back (WB): sólo se modifica la copia de la memoria cache, y se 
mantiene la memoria principal con el valor antiguo. El objetivo es 
reducir el número de accesos a memoria, y con ello el tráfico en el bus 
y la latencia de las operaciones. Es la estrategia que habitualmente 
usan los procesadores.Por tanto, el sistema de datos no es coherente (ambas copias no son 
iguales), y en algunos momentos será necesario recuperar la 
coherencia, es decir, actualizar la memoria principal, normalmente al 
eliminar el bloque de la cache (por ejemplo, por reemplazo). Para 
gestionar los bloques de datos se utilizan algunos bits de control en el 
directorio de la cache, que indican el “estado” del bloque de datos. Es 
suficiente con dos bits: valid, para indicar que la información 
almacenada es útil; y dirty, para indicar que está modificada. 
 
Aunque hemos dicho que es el procesador el único dispositivo que tiene 
capacidad de modificar una copia, lo que facilita mucho la gestión de las 
mismas, no es estrictamente cierto, ya que en las operaciones de 
entrada/salida, por DMA por ejemplo, es un controlador especial el que toma 
control del bus y de la operación de escritura. En esa operación se 
modificarán varios bloques de datos; ¿qué habría que hacer con las posibles 
copias de esos datos en la cache? En algunos casos el problema desaparece 
porque se declaran como no “cacheables” los bloques de datos de E/S (nunca 
se copian en cache, y todos los accesos se hacen en memoria principal); si no 
es así, será el sistema operativo el que tenga que tomar control de esa 
operación y mantener la coherencia (flush de la cache). En todo caso, las 
operaciones de E/S son de muy baja frecuencia en comparación con las 
operaciones del procesador sobre la cache. 
3.1.2 Coherencia de los datos en los multiprocesa-
dores de memoria compartida (SMP) 
El problema de la coherencia es mucho más peliagudo en los 
multiprocesadores de memoria compartida. La comunicación entre procesos 
se realiza mediante el uso de variables compartidas. Cada procesador usará 
su propia cache local, en la que tendrá copia de dichas variables, por lo que 
las copias potenciales de un bloque de datos no serán 2 (las correspondientes 
▪ 86 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
a la jerarquía de memoria) sino P+1, siendo P el número de procesadores. 
Además, y aquí está el problema principal, cualquier procesador puede 
efectuar una modificación en dichos bloques, en su cache local. Tal como 
hemos visto, las políticas de escritura WT o WB permiten gestionar la 
coherencia de los datos en el caso de un procesador, pero ¿cómo hacer lo 
mismo con el resto de las memorias cache del multiprocesador? ¿cómo saber 
si otro procesador ha modificado el bloque en su cache, y por tanto ya no es 
válida nuestra copia? Por definición, el problema de coherencia sólo existe 
con los datos compartidos; con los datos privados el problema se resume al 
de un solo procesador. 
Como hemos comentado antes, el problema desaparece si se decide no 
llevar a las caches las variables compartidas (por ejemplo, las que se utilizan 
para la comunicación entre procesos), dado que no se harán copias, pero 
dicha decisión puede tener un efecto severo en el rendimiento del sistema, ya 
que todos los accesos de los procesadores a dichas variables tendrán que 
hacerse en memoria principal: crecerá mucho el tráfico en el bus y, en 
consecuencia, debido a los conflictos en el acceso al bus, subirán los tiempos 
de respuesta. Algo de ello se muestra en el siguiente ejemplo. 
 
La velocidad de transferencia del bus de un multiprocesador es 1 GB/s y el reloj es de 
800 MHz. Los procesadores ejecutan una instrucción por ciclo, y el 2% de las mismas 
son operaciones de memoria, LD/ST, sobre variables compartidas. Los datos son de 8 
bytes. ¿Cuántos procesadores pueden conectarse en el bus sin llegar a saturarlo si las s 
compartidas se dejan en la memoria principal? 
 
En cada segundo hay que transferir 800 106 ciclos × 0,02 instr. (LD/ST) × 8 bytes 
= 128 MB por procesador, considerando sólo los datos compartidos. 
Por tanto, 8 procesadores generarán un tráfico de 1.024 MB/s para acceder a las 
variables compartidas, el máximo que admite el bus. 
 
Ineludiblemente, necesitamos una estrategia que permita disponer de 
copias en las caches locales de los procesadores y que éstos las puedan 
modificar. Ya sabemos que el uso de las caches (de copias, por tanto) ofrece 
dos grandes ventajas: los tiempos de acceso a memoria son menores y se 
reduce el tráfico en el bus. 
3.1.3 Falsa compartición 
El problema de coherencia aparece con las variables compartidas. Las 
variables privadas sólo estarán, como mucho, en una cache, y no significan 
ningún problema nuevo. 
3.1 PRESENTACIÓN DEL PROBLEMA Y REVISIÓN DE CONCEPTOS ▪ 87 ▪ 
 
No hay que olvidar, sin embargo, que el control del contenido de la cache, 
y el de la coherencia en concreto, se hace por bloques, no palabra a palabra: 
se cargan bloques de datos, se borran bloques, se anulan bloque, etc. Por 
ello, es posible que un bloque de datos se encuentre en más de un 
procesador, aunque todas las variables del bloque sean privadas. Por 
ejemplo: 
 
Bloque de datos de 4 palabras 
 
X Y Z T 
 
 
 
Aunque las variables son privadas están en el mismo bloque de datos, por 
lo que el bloque será compartido y tomará parte en las operaciones de 
coherencia. Se dice que hay un problema de falsa compartición (false 
sharing). Para evitar este efecto es necesario distribuir los datos en memoria 
de manera adecuada y es útil que los bloques de datos no sean muy grandes. 
3.1.4 Definición de la coherencia 
Decimos que un sistema es coherente si al leer una variable se obtiene 
siempre como resultado el último dato que se escribió en dicha variable. En 
esta definición no muy formal, se introducen dos conceptos: la propia 
coherencia —qué valor se obtiene—, y la consistencia —cuándo se verá en 
la variable el valor que ha escrito otro procesador—. Ésta segunda cuestión 
la analizaremos un poco más adelante. 
Se asegura la coherencia de un sistema de memoria si se cumplen las tres 
siguientes condiciones: 
 
1. Desde el punto de vista de un solo procesador, el resultado de una 
lectura (LD) debe ser siempre el correspondiente a la última escritura 
efectuada por ese procesador en esa variable (siempre que ningún otro 
procesador haya modificado dicha variable). Es decir, hay que 
respetar el orden entre LD y ST (sobre la misma variable). Se trata de 
una condición que también hay que cumplir en el caso de los sistemas 
con un solo procesador. 
Variables del 
procesador Pi 
Variables del 
procesador Pj 
▪ 88 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
2. Considerando todos los procesadores, la operación Pi_rd_A debe 
devolver siempre lo escrito por la última operación Pj_wr_A, si es que 
ha pasado "suficiente tiempo" desde que se realizó. Ésta es, más o 
menos, la definición de coherencia: todos los procesadores tienen que 
conocer los cambios producidos en el resto. 
 
3. Las escrituras (cambios) sobre una variable tienen que verse en el 
mismo orden en todos los procesadores. 
 
Las estrategias y mecanismos que se han desarrollado para mantener la 
coherencia de los datos son diferentes en función de la arquitectura de la 
máquina. Como hemos visto en el capítulo anterior, tenemos dos opciones 
para los sistemas de memoria compartida: multiprocesadores SMP — de 2 a 
16 procesadores conectados en un bus—; o computadores DSM —muchos 
procesadores conectados mediante una red de comunicación más 
sofisticada—. En el primer tipo de arquitecturas se utilizan protocolos de 
coherencia tipo snoopy, mientras que en el segundo se utilizan protocolos 
basados en directorios. 
En este capítulo vamos a analizar los protocolos de coherencia más 
habituales en los sistemas SMP (y en el capítulo 7 analizaremos los 
directorios de coherencia). 
 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY 
Como hemos comentado en el capítulo anterior, la memoria de los 
sistemas SMP está "concentrada" en un solo sitio, y los procesadores utilizan 
normalmente un bus16 para acceder a memoria. En este tipo de sistema, la 
coherencia de los datos se mantiene por hardware, por medio deun 
dispositivo que se conoce como snoopy (fisgón). Puesto que la memoria y 
los procesadores se conectan mediante un bus, una red centralizada, todas las 
operaciones con la memoria principal son “públicas”, es decir, que cualquier 
procesador puede ver lo que otros están haciendo (LD, ST) dado que también 
él está conectado al bus. La función del snoopy es justamente ésa: espiar en 
todo momento el bus para enterarse de las operaciones que realizan otros 
procesadores, y, en su caso, distribuir por el bus información de control. En 
 
16 Vamos a utilizar el modelo más simple de bus, en el que sólo se procesa una petición de uso del bus y 
no se admite otra hasta finalizar con la anterior. En general, los buses de los sistemas multiprocesador 
son más complejos. 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 89 ▪ 
 
función de la información que obtenga, el snoopy decidirá qué hacer con los 
bloques de datos que tiene en la cache local. 
Cuando se modifica un determinado bloque de datos en la cache, ¿qué hay 
que hacer con el resto de posibles copias del mismo en los otros 
procesadores? Tenemos dos alternativas: 
 
▪ Invalidar todas las copias de ese bloque que existan en el resto de 
memorias cache, y dejar por tanto una única copia, la que se ha 
modificado. 
 
▪ Actualizar todas las copias de ese bloque, enviando a través del bus el 
nuevo valor de la palabra modificada. 
 
En la siguiente figura aparece un ejemplo de ambas alternativas. 
 
 
 
 
 
 
 
 
 Invalidación Actualización 
 
En el primer caso, el procesador P1 va a modificar la variable A en su 
cache, de 4 a 3. Efectúa la escritura y envía una señal de control especial al 
bus, INV, para invalidar la copia de P2; como consecuencia de ello, sólo 
permanecerá en las caches la copia de P117. En el segundo caso en cambio, 
se distribuye a todos los procesadores el nuevo valor de la variable A, 
mediante una señal de control especial, BC –broadcast–, para que la 
actualicen en su cache. Se mantienen por tanto todas las copias. 
Tanto en un caso como en el otro, la memoria principal se actualizará o no 
en función de la política de escritura que se utilice: en todas las escrituras si 
se usa WT, y sólo en algunas ocasiones si se utiliza WB. 
Ya hemos comentado que la coherencia de los datos se mantiene por 
bloque, y para ello se añaden algunos bits de control a los bloques de datos 
en el directorio de la cache. Mediante esos bits se definen diferentes estados 
 
17 Aunque en el ejemplo sólo aparece una palabra, un bloque contiene siempre varias palabras. 
MC1 MC2 
wr A,#3 
MP 
 
P1 
 
P2 
INV A 
 
 
A = 4→3 
 
A = 4 
 
A = 4→3? 
MC1 MC2 
wr A,#3 
MP 
 
P1 
 
P2 
BC A,3 
 
A = 4→3 
 
A = 4→3 
 
A = 4→3? 
▪ 90 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
para los bloques. Un autómata finito (el snoopy) se encargará en cada cache 
de ir modificando los estados de los bloques en función de las operaciones 
que se realicen, tanto desde el procesador local como desde el resto de los 
procesadores, sobre los mismos. 
3.2.1 Estados de los bloques en la memoria cache y 
señales de control 
Para mantener la coherencia de los datos en la cache se suelen utilizar 
cinco estados. No es necesario utilizarlos todos, y en muchos casos sólo se 
usan algunos de ellos, como vamos a ver. Los estados se definen de acuerdo 
a dos características: el número de copias de un bloque y si el bloque es o no 
coherente (igual) con la copia de memoria principal (los nombres de los 
estados pueden variar de máquina a máquina). 
 
I Inválido (invalid) 
 Un bloque está en estado I si la información que contiene no es válida; 
es lo mismo que si no estuviera en la cache (un fallo de cache). 
 Para indicar que un bloque no está en la cache, utilizaremos también 
el símbolo (-). Por ejemplo, cuando se reemplaza un bloque no se 
anula, simplemente desaparece. En definitiva, ambos casos, I o (-), 
son completamente equivalentes. 
 
E Exclusivo (exclusive, reserved) 
 Un bloque está en estado E si se trata de la única copia en todas las 
caches del multiprocesador y si además su contenido es el mismo que 
el del bloque en memoria principal, es decir, es coherente. 
 
M Modificado (modified, dirty, exclusive rd/wr) 
 Un bloque en estado M es la única copia existente en el 
multiprocesador, pero no está actualizado en memoria principal: se ha 
escrito en la cache pero no en memoria principal (write-back). 
 
S Compartido (shared, shared rd only) 
 Existen (o pueden existir) múltiples copias de dicho bloque en el resto 
de las caches del multiprocesador, y todas las copias son iguales entre 
sí y, normalmente, iguales con la copia de memoria principal 
(coherentes). 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 91 ▪ 
 
O Propietario (owner, shared dirty) 
 Existen (o pueden existir) múltiples copias de dicho bloque en el resto 
de las caches del multiprocesador, pero, aunque entre ellas son 
iguales, el bloque no está actualizado en memoria principal. La copia 
en estado O será la encargada, en su momento, de actualizar la 
memoria principal y mantener así la coherencia (por ejemplo, al ser 
reemplazada). El resto de copias, si existen, se encuentran en estado S 
(atención, esas copias no son coherentes con memoria principal). 
 
Para definir los estados del bloque basta con usar tres bits. Por ejemplo: 
 
válido 
(valid) 
modificado 
(dirty) 
compartido 
(shared) 
 Estado 
0 – – I 
1 0 0 E 
1 0 1 S 
1 1 0 M 
1 1 1 O 
 
Como hemos comentado al principio, los dos primeros bits son los 
mismos que se utilizan en los sistemas de un solo procesador, por lo que, dfe 
momento, sólo se añade un bit más al directorio. 
Una máquina de estados finitos en cada procesador, el snoopy, se encarga 
de mantener los estados de los bloques de datos en la cache de acuerdo a la 
definición anterior, para lo que hay que tomar en consideración las 
siguientes acciones: 
 
1. Acciones del procesador local 
PR: processor read 
Se lee una variable (en un bloque de datos). Si el bloque está en 
la cache (acierto), no hay que hacer nada; pero si no está (fallo) 
hay que generar una petición de lectura de ese bloque (BR, bus 
request). 
 
PW: processor write 
Se escribe en una variable (un bloque de cache). En general, hay 
que avisar a los otros procesadores, para que actualicen el estado 
de dicho bloque en su cache: INV (invalidar) o BC (actualizar), 
en función del tipo de protocolo. Además, si ha sido un fallo, hay 
que pedir el bloque de datos correspondiente. 
▪ 92 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
2 Acciones que se observan en el bus (señales de control enviadas por 
los otros snoopy), como resultado de operaciones de otros 
procesadores. El número, nombre y tipo de las señales depende de la 
implementación del protocolo. En nuestro caso, usaremos las 
siguientes: 
 
BR: bus read 
Un procesador quiere leer una palabra y se ha producido un fallo 
en su cache (no está). Tienen por tanto que conseguir el bloque 
correspondiente, y para ello se genera esta petición (BR en el bus 
de control, y la dirección en el bus de direcciones). Todos los 
snoopy locales tienen que considerar esta señal para adecuar el 
estado del bloque (si tienen una copia del mismo). 
INV: invalidate [ en los protocolos de invalidación ] 
Se escribe una palabra en la cache y, por tanto, hay que eliminar 
todas las copias de dicho bloque. Se envía al bus de control la 
señal INV y al bus de direcciones la dirección del bloque a 
anular. Todos los snoopy tienen que responder adecuadamente a 
la señal, anulando, en su caso, la copia del bloque. 
 
BC: broadcast [ en los protocolos de actualización ] 
Se escribe una palabra en la cache, por lo que hay que actualizar 
todas las copias de dicha variable. Se activa la señal BC en el bus 
de control, y se pone la dirección de lavariable en el de 
direcciones y nuevo valor en el de datos). Todos los snoopy 
tienen que responder adecuadamente a la señal, actualizando, en 
su caso, la variable correspondiente. 
 
En algunos casos hay que activar más de una señal de control; por 
ejemplo, en un fallo en escritura: hay que solicitar el bloque de datos 
(BR) y anular o actualizar el resto de copias (INV o BC). Las señales que 
acabamos de definir son simplemente una opción, y las implementaciones 
de las mismas pueden ser diferentes; por ejemplo, en lugar de activar dos 
señales de control a la vez, puede utilizarse una tercera señal que indique 
ambas acciones: RdEx (o BRinv), "lectura exclusiva". 
 
3. Otras señales de control 
Las acciones anteriores tienen como consecuencia que se modifique el 
estado de los bloques en la cache. Además de ellas, también 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 93 ▪ 
 
aparecerán en el bus las siguientes acciones, que no tienen efecto 
sobre el estado de los bloques: 
 
BW: bus write 
Un procesador va escribir un bloque entero de datos en memoria 
principal. Esto va a ocurrir en los casos en los que la política de 
escritura sea write-back, cuando es necesario actualizar datos o 
en los reemplazos de bloques modificados. 
BW*: Un procesador va a escribir una palabra en memoria principal 
(estamos usando por tanto WT). Esta señal de control no es 
estrictamente necesaria, ya que puede utilizarse para ello la señal 
INV (o BC), porque al escribir la memoria principal también hay 
que invalidar (o actualizar) el resto de copias (usaremos el * para 
indicar una transferencia de sólo una palabra). 
 
Un protocolo de coherencia snoopy es un algoritmo distribuido en el que 
colaboran P autómatas finitos distribuidos en P procesadores. Utilizando los 
estados que acabamos de describir, permite trabajar con múltiples copias de 
un bloque de datos. El snoopy debe controlar las peticiones y avisos que le 
lleguen de su procesador local o del resto a través del bus, y, en función de 
ellas, decidir el estado de los bloques de datos y generar las señales de 
control adecuadas. 
Pueden definirse muchos algoritmos de coherencia diferentes, utilizando 
algunos o todos los estados anteriores, y diferenciándose entre ellos por la 
política de escritura: write-through, write-back, o mezclas de ambos (en 
función del número de copias, del número de escrituras, de la jerarquía de 
cache, etc.). En muchos textos, el nombre de estos protocolos hace referencia 
a los estados que utilizan: MESI, MOSI, etc. 
3.2.2 Protocolos de invalidación 
Cuando se realiza una escritura en un bloque, y la coherencia se mantiene 
mediante un protocolo de invalidación, se eliminan todas las copias de ese 
bloque que haya en el sistema. Los protocolos más simples son de sólo dos 
estados (I-E o I-S) y utilizan como política de escritura WT, pero no son 
demasiado eficientes. Por ello, vamos a analizar protocolos de al menos tres 
estados y que utilizan WB como política de escritura siempre que es posible. 
▪ 94 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
3.2.2.1 Un protocolo de tres estados, MSI (Silicon Graphics) 
Uno de los protocolos más comunes de tres estados es el que se ha 
utilizado en algunos de los computadores de Silicon Graphics. Los posibles 
estados de los bloques en cache son I, M y S; la política de escritura es, por 
tanto, write-back (se utiliza el estado M), aunque no se admite más de una 
copia de un bloque que no esté actualizado en la memoria principal. 
Para definir un protocolo de coherencia (una autómata de estados finitos), 
hay que definir las transiciones entre los estados de los bloques de datos y 
las señales de control que se generan en dichas transiciones, en función, por 
un lado, de las acciones del procesador local (PR y PW), y, por otro, de las 
acciones del resto de procesadores, reflejadas en las señales de control que se 
detecten en el bus (BR e INV). Todo ello se refleja en la siguiente tabla: 
 
 Estado 
presente 
Estado siguiente / Señales de control 
PR PW BR INV 
fa
llo
 
I, - S BR M BR,INV 
ac
ie
rto
 S S M INV S I 
M M M S BW I BW 
 
 
 
 
 
 
Tráfico (datos) 
 MP → MC: BR // I → S, M 
 MC→ MP: BW // M → S, I (+reemplazo) 
 
 
Los protocolos también se pueden representar mediante un grafo, tal como 
aparece en la siguiente figura. 
 
 
 
 
 
 
 
 
 
 
 
 
PW (BR,INV) 
PR (BR) 
PW (INV) 
INV (BW) 
BR (BW) 
INV 
PR - PW 
PR - BR 
 
M 
 
S 
 
I, - 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 95 ▪ 
 
Las líneas continuas (letra en negrita) representan las transiciones 
generadas por acciones del procesador local, y las flechas discontinuas las 
que se producen como consecuencia de las señales de control que aparecen 
en el bus, debidas a lecturas y escrituras de otros procesadores. Entre 
paréntesis y en cursiva aparecen las señales de control que se envían al bus. 
Cuando el procesador lee una variable que está en la cache (PR), el estado 
del bloque no se modifica ni se generan señales de control. En cambio, si la 
variable no está en la cache (I), hay que pedir el bloque de datos 
correspondiente, generando para ello la señal de control BR; una vez que 
obtengamos el bloque, se carga en la cache en estado S (no se puede poner 
en estado M porque no ha sido modificado). 
En caso de escritura (PW), el estado del bloque pasará a ser M: una única 
copia y modificada (write-back). Si ya estaba en estado M no hay que hacer 
nada; pero si estaba en estado S hay que invalidar todas las posibles copias18 
(mediante la señal de control INV). Si la escritura ha sido un fallo (estado I, 
la variable no está en la cache), antes de escribir se debe conseguir el bloque, 
en modo exclusivo, para lo que se generan las señales de control BR e INV 
(BR: leer el bloque + INV: invalidar todas las copias). 
Veamos ahora las consecuencias de las operaciones realizadas por otros 
procesadores y que se detectan en el bus. Un procesador ha solicitado un 
bloque de datos, para lo que activado la señal BR. El bloque solicitado podría 
estar en la cache local, en estado S o M. Si está en estado S, no hay que 
hacer nada: a las copias que ya había antes, coherentes, se le añade una más. 
Pero si está en estado M, es decir, si la copia local es la única y no está 
actualizada, hay que modificar su estado. A partir de ahora habrá dos copias 
en el sistema, y la única opción en este protocolo es pasar al estado S, es 
decir, pasar a ser coherente: el nuevo estado es S y hay que actualizar 
(escribir) el bloque en la memoria principal (BW). 
Finalmente, si se detecta la señal INV en el bus, la decisión es muy 
simple: si el bloque de datos está en la cache, hay que eliminarlo (I). Dado su 
efecto, la señal de invalidación INV tiene preferencia frente a la señal BR 
cuando ambas se activan a la vez. Como la política de escritura es write-
 
18 El estado S no implica que necesariamente tenga que haber más copias en el sistema; es decir, 
aunque es seguro que en algún momento sí ha habido más de una copia, pueden haber sido 
reemplazadas todas ellas, quedando una sola copia, en estado S. Además, en este protocolo la primera 
copia también se carga en estado S, ya que no se utiliza el estado E (una sola copia). 
▪ 96 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
through, es posible que el bloque que hay que borrar esté en estado M, en 
cuyo caso habrá que actualizar su contenido en MP. 
 
▪ Tráfico en el bus compartido 
 
El tráfico que se genera en el bus que conecta los procesadores y la 
memoria en un multiprocesador es un aspecto crítico en el rendimiento del 
sistema. Dado que es un recurso compartido, el bus puede saturarse; en ese 
caso, la latencia de las comunicaciones con memoria crecerá, y la velocidad 
de cálculo bajará. Por ello, un protocolo de coherencia adecuado debe 
intentar reducir dicho tráfico, para que se pueda conectarel mayor número 
de procesadores al bus. 
En la parte inferior de la tabla de transiciones de estados del protocolo se 
muestra el tráfico en el bus de datos. Hay que transferir un bloque de datos 
en estos dos casos: de memoria principal a memoria cache al generarse la 
señal BR (es decir, cuando un bloque pasa de estado I a S o M); y de 
memoria cache a memoria principal cuando se genera la señal BW (cuando 
un bloque en estado M pasa a estado S, se anula o se reemplaza). 
 
▪ ¿De dónde se traen los bloques de datos? 
 
Cuando hay que cargar un bloque en la cache, normalmente se traerá de 
MP. Sin embargo, en algunos casos ese bloque se puede traer de alguna otra 
cache (porque hay una copia del mismo). Esta posibilidad no disminuye el 
tráfico en el bus, pero sí el tiempo de acceso, porque traerlo desde otra cache 
va a ser más rápido. En cualquier caso, hacer esto genera una interferencia 
en el funcionamiento de otro procesador (mientras se está realizando la copia 
de cache a cache, no podrá utilizar su memoria cache), además de necesitar 
un arbitraje para escoger una determinada copia, por lo que habitualmente se 
trae el bloque de MP. 
Si el bloque que se quiere traer está en estado M en otra cache, el snoopy 
tiene que conseguir esa copia, ya que la MP no está actualizada. Como en el 
caso anterior, tenemos dos opciones: actualizar primero la MP, y luego leer 
ahí el bloque; o copiar el bloque en la cache que lo necesita a la vez que se 
está actualizando la MP (por tanto, el bloque está en el bus): 
 
(a) MC1 (M) → MP → MC2 o (b) MC1 (M) → MP 
 → MC2 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 97 ▪ 
 
La segunda opción es bastante más adecuada ya que reduce a la mitad el 
tráfico en el bus y la latencia de la operación. 
En el caso de que quien solicita el bloque vaya a efectuar una escritura, se 
podría eliminar la escritura del bloque en MP, y efectuar únicamente la 
transferencia MC1 (M) → MC2 (M), ya que, después de todo, se va a cargar 
en la cache y se va a modificar. 
 
3.2.2.2 El protocolo Illinois, MESI (Papamarcos&Patel, 84) (M88100) 
Veamos otro conocido protocolo, Illinois, utilizado (con algunas 
modificaciones) en los procesadores Pentium, PowerPC y MIPS R4400. Es 
una mejora del protocolo anterior, al que se le añade un cuarto estado, E, con 
el objetivo de minimizar el número de invalidaciones. 
El estado E nos asegura que en todo el sistema sólo hay una copia del 
bloque (recordemos que el estado S no distingue entre el número de copias 
que hay del bloque) y que, además, es coherente con la información que hay 
en la memoria principal. Para distinguir entre los estados E y S se introduce 
una nueva señal de control en el bus –sh (shared)–, que indica si un bloque 
concreto se encuentra en alguna otra cache o no (es decir, si se está cargando 
una copia única o ya había al menos una copia previamente en el sistema). 
La tabla de transiciones correspondiente al autómata de coherencia es la 
siguiente: 
 
 Estado 
presente 
Estado siguiente / Señales de control 
PR PW BR INV 
fa
llo
 
I, - nsh: E sh: S BR M BR,INV 
ac
ie
rto
 
E E M S I 
S S M INV S I 
M M M S BW I BW 
 
 
 
 
 
 
Tráfico (datos) 
 MP → MC: BR // I → E, S, M 
 MC → MP: BW // M → S, I (+reemplazo) 
 
▪ 98 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Comparado con el caso anterior, la principal diferencia estriba en que un 
bloque que se lee (BR) se carga en la cache en estado E (coherente) si 
sabemos que es la única copia de dicho bloque en todo el sistema, es decir, si 
no está en ninguna otra cache; si no, se cargará en estado S. Para saber si hay 
copias o no, se utiliza la línea de control sh, de manera que cuando aparece 
en el bus la señal BR todos los snoopy mirarán en los directorios de sus 
caches para comprobar si tienen una copia de ese bloque o no, y, en caso 
afirmativo, activarán la señal sh. Por tanto, si sh = 1 existen copias del 
bloque (al menos una) en otras caches, y si sh = 0 (nsh, not shared), se está 
cargando en el sistema la primera copia de dicho bloque. 
Cuando se carga en el sistema la segunda copia, ambas pasarán a estar en 
estado S, y a partir de ahí la evolución del bloque será la misma que la que 
hemos analizado en el protocolo anterior. 
El objetivo del estado E es distinguir los bloques privados (siempre serán 
copias únicas) de los compartidos, y, así, reducir el tráfico en el bus. Si se 
hace una escritura sobre un bloque que está en estado E, el bloque pasará al 
estado M, sin generar tráfico (si hubiera estado en estado S, tendríamos que 
haber activado la señal de invalidación junto con la dirección del bloque). Es 
decir, no se envía la señal INV cuando no hay copias del bloque que se va a 
modificar. No hay que olvidar que la mayoría de los bloques de datos serán 
privados, y sólo algunos de ellos serán compartidos. 
 
 
PW (BR,INV) 
INV (BW) 
INV 
PW (INV) 
BR (BW) 
PW 
PR 
PR - PW 
BR 
PR - BR 
 
I, - 
 
S 
 
M 
 
E 
PR (BR) 
INV 
sh nsh 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 99 ▪ 
 
3.2.2.3 El protocolo Berkeley, MOSI 
Como último ejemplo de protocolos de invalidación, analicemos el 
protocolo Berkeley; utiliza los estados I, M, S y O, y la política de escritura 
es write-back "siempre". Recuerda que el estado O (propietario, owner) se 
utiliza para poder tener múltiples copias de un bloque no coherente con 
memoria principal (en los dos protocolos anteriores sólo se permitía una 
copia no coherente, en estado M). Como no se diferencian los estados E y S, 
no se usa la señal sh. La tabla de transiciones y el grafo del protocolo son 
las siguientes: 
 
 Estado 
presente 
Estado siguiente / Señales de control 
PR PW BR INV 
fa
llo
 
I, - S BR M BR,INV 
ac
ie
rto
 
S S M INV S I 
M M M O I BW 
O O M INV O I BW 
 
 
 
 
 
 
Tráfico (datos) 
 MP / MC → MC: BR // I → S, M 
 MC → MP: BW // M, O → I (+reemplazo) 
 
 
 
 
 
 
 
 
 
 
 
 
Con el nuevo estado O es posible tener múltiples copias de un mismo 
bloque no coherentes con MP pero coherentes entre sí (esto permite utilizar 
una política de escritura write-back en todos los casos). En el protocolo 
anterior había que efectuar la transición M → S cuando era requerida una 
PR - BR 
 
I 
 
O 
BR 
PR - BR 
PW (BR,INV) 
INV (BW) 
INV 
PW (INV) 
INV (BW) 
PR (BR) 
PW (INV) 
PR - PW 
M 
 
S 
▪ 100 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
segunda copia de un bloque; ahora, en cambio, el cambio de estado será M 
→ O, y no se actualizará la MP. Con esto se consigue no tener que actualizar 
un bloque en MP hasta que el bloque sea invalidado o reemplazado. 
Cuando un bloque está en estado M en una cache, y se produce una 
lectura de ese bloque en otra cache, el primero pasará a estado O 
(propietario) y la copia nueva se cargará en estado S. ¡Cuidado! El estado S 
no implica que el bloque sea coherente con memoria principal. Si las copias 
de ese bloque están en estado S en todas las caches del multiprocesador, 
entonces serán coherentes con MP; pero si una de las copias está en estado 
O, entonces las copias serán coherentes entre sí, pero no lo serán con MP. 
Cuando una copia está en estado O y tiene que ser reemplazada, el snoopy 
correspondiente tiene que actualizar la MP; tras ello, el resto de copias de 
ese bloque, que estarán en estado S, retomarán la definición inicial de estado 
S (coherentes con MP). 
En lo que al tráfico se refiere, no hay ningún cambio sustancial respecto al 
protocolo anterior. Cuando se tiene que cargar un bloque en estado S en una 
cache (fallo en lectura), hay que tener en cuenta dos posibilidades. Si el 
bloque no existe en ninguna otra cache, necesariamente habrá que traerlo de 
MP; pero si está en alguna otra cache en estado O o M, habrá que traerlo de 
esa cache, ya que la MP no está actualizada. El controlador de coherencia de 
esa copia deberá de responderadecuadamente a la petición, pasando los 
datos de la memoria cache al bus para que se puedan leer y cancelando la 
lectura que se había solicitado a la MP. 
3.2.2.4 Resumen de los protocolos de invalidación 
En los apartados anteriores hemos analizado algunos protocolos de 
coherencia de invalidación, en los que las copias de un bloque se invalidan 
cuando se modifica una de ellas. Entre ellos se diferencian por los estados 
que utilizan, la política de escritura, etc. 
No hemos descrito todos los que existen, ni mucho menos. No hay 
problema, por ejemplo, para definir un protocolo de invalidación que utilice 
los cinco estados. Otro ejemplo bastante conocido es el protocolo write-
once, en el que se utilizan ambas políticas de escritura (WT y WB) según los 
casos: WT cuando se escribe por primera vez en el bloque y WB para las 
sucesivas escrituras. También se pueden definir protocolos que tienen en 
cuenta la jerarquía de memoria a la hora de definir los estados (por ejemplo, 
el procesador Alpha). Todos ellos se dejan como ejercicio. 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 101 ▪ 
 
3.2.3 Protocolos de actualización 
Otros protocolos que se utilizan para gestionar las diferentes copias de un 
bloque que puede haber en un multiprocesador de memoria compartida se 
engloban dentro del grupo de protocolos de actualización. En los protocolos 
de invalidación, cuando una de las copias se va a modificar se elimina el 
resto de las copias. Ahora, en cambio, se van a mantener el resto de las 
copias, pero actualizadas. El control, snoopy, de la cache que va a hacer la 
escritura deberá informar al resto de las caches del cambio realizado, y éstas 
actualizarán el bloque con el nuevo valor de la variable. 
Para poder actualizar una variable en el resto de las caches, se utiliza una 
señal de control denominada BC (broadcast), y junto con ella se pondrá en el 
bus la dirección de la variable y el nuevo dato. Los controladores de las 
caches procesarán esas señales y, cuando corresponda, realizarán los 
cambios asociados a la escritura, tanto en el valor de la variable como en el 
estado del bloque. 
A pesar de que pudiera parecer que siempre obtendríamos mejores 
resultados con este tipo de protocolos, en realidad va a depender de la 
aplicación que se esté ejecutando. En los casos en los que un bloque de datos 
se reutilice sistemáticamente en los diferentes procesadores, la actualización 
será más eficiente, pues se mantiene la copia del bloque en las caches; en 
cambio, si el bloque no se va a reutilizar, quizás se esté actualizando ese 
bloque sin sacar ningún rendimiento a esas actualizaciones (hubiera sido 
mejor invalidar el bloque la primera vez). No hay que olvidar que para 
transmitir los datos de la actualización hay que utilizar el bus y, además, 
mientras se está actualizando una cache el procesador local no puede 
utilizarla y deberá esperar. 
Los protocolos de actualización no invalidan los bloques y, por tanto, no 
utilizan el estado I. Sin embargo, es necesario utilizar el estado I para otras 
cuestiones, tales como, por ejemplo, para invalidar los bloques de datos en 
los cambios de contexto o en migraciones de procesos. Por ello, para 
representar el caso de que un bloque no esté en la cache utilizaremos (I, -). 
Veamos dos protocolos de actualización bastante conocidos. 
3.2.3.1 El protocolo Firefly, MSE(I) (Archibald & Baer 85) (DEC) 
Este protocolo de actualización utiliza los estados E, M y S. La política de 
escritura es write-back con los bloques privados y write-through con los 
▪ 102 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
compartidos. Esto es, cuando sólo hay una copia de un bloque, en las 
escrituras no se actualiza la memoria principal; en cambio, cuando hay 
varias copias del bloque, todas las escrituras actualizan también la memoria 
principal. Como el protocolo distingue entre los estados E y S, el bus de 
control cuenta con la señal sh (shared): sh = 1 → hay copias de dicho 
bloque en alguna otra cache; sh = 0 → no hay copias. 
Las transiciones entre estados y las señales de control de este protocolo se 
muestran en la siguiente tabla y en el grafo correspondiente: 
 
 
Estado 
presente 
Estado siguiente / Señales de control 
PR PW BR BC 
fa
llo
 
- nsh: E 
 sh: S 
BR nsh: M 
 sh: S 
BR 
BR,BC 
ac
ie
rto
 
E E M S S 
S S nsh: E 
 sh: S 
BC S S 
M M M S BW S BW 
 
 
 
 
 
 
Tráfico (datos) 
 MP / MC → MC: BR // (I) → E, S, M 
 MC → MP: BW // M → S (+reemplazo) 
 MC → MC*MP*: BC // (I) → S(wr); S → E, S(wr) 
 
 
 
 
 
 
 
 
 
 
 
Cuando se va a cargar un bloque nuevo en una cache, el estado del bloque 
va a depender de la señal sh. Si se detecta que no hay copias del bloque en 
el sistema (nsh), el estado será E (en las lecturas) o M (en las escrituras); a 
 
M 
 
E PR 
BR - BC 
 
 
BR (BW) 
PW (BC) 
PR 
PW 
 
S 
PR - PW 
nsh 
sh 
BR 
PR (BR) 
PW (BR) 
( - ) 
PW (BC) nsh 
nsh 
sh 
( - ) 
sh 
 (BC) 
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 103 ▪ 
 
ambos estados, que indican que sólo hay una copia del bloque de datos, se 
les aplica la política de escritura write-back. En cambio, si se detecta que hay 
una o más copias del bloque en el sistema (sh), el estado del nuevo bloque 
será S, y la posterior política de escritura será write-through, que mantiene 
coherentes la memoria principal y las memorias cache. 
Del mismo modo, si se escribe sobre un bloque que está en estado S, se 
elegirá entre E o S en función de la señal sh. Ten en cuenta que aunque el 
bloque esté en estado S (compartido), puede ser que en ese momento sea la 
única copia si se han reemplazado las demás; aprovechamos así la escritura 
para actualizar el estado (aunque sólo quede una copia, el estado será E y no 
M, porque la política de escritura con las copias compartidas es siempre la 
misma: hay que actualizar la memoria principal). 
Desde el punto de vista del tráfico, el caso más interesante es la transición 
(I, -) → S. Si es consecuencia de una lectura, entonces hay que traer el 
bloque a la cache, bien desde MP o bien desde otra cache. Si el resto de las 
copias son coherentes (E, S), normalmente se traerá de MP; si no son 
coherentes (M), entonces antes de traer el bloque (o a la vez) habrá que 
actualizar la memoria principal. Por otro lado, en las transiciones (I, -) → S, 
cuando son consecuencia de una escritura, además de traer el bloque hay que 
actualizar la memoria principal y todas las copias del mismo. Por tanto, 
cuando se genera la señal BC también hay que actualizar (una palabra) la 
memoria principal (es decir, cumple la misma función que la señal BW* que 
definimos anteriormente). Por ello, para reducir el tráfico de actualización, 
en el caso escritura/fallo antes de generar la señal BC se espera a obtener la 
respuesta de la señal sh; si no, podríamos genera la señal BC desde el 
comienzo de la operación. 
 
3.2.3.2 El protocolo Dragon, MOES(I) (McCreight 84, Xeroc Parc Dragon) 
En este protocolo se utilizan todos los estados: E, M, S y O (una variación 
de este protocolo se utiliza en las máquinas Sun Sparc/Server). Al igual que 
en el protocolo Berkeley, el estado O permite aplicar la política write-back 
en todos los casos. Se mantiene la señal sh (shared), para distinguir entre 
los estados E y S y así poder reducir el número de actualizaciones (BC). 
En la siguiente tabla y su grafo correspondiente se presenta este protocolo. 
 
 
▪ 104 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
 
 Estado 
presente 
Estado siguiente / Señales de control 
PR PW BR BC 
fa
llo
 
- nsh: E 
 sh: S BR 
 nsh: M 
 sh: O 
BR 
BR,BC 
ac
ie
rto
 
E E M S S 
S S nsh: M 
 sh: O 
- 
BC S S 
M M M O S 
O O nsh: M 
 sh: O 
- 
BC O S 
 
 
 
 
 
 
 
Tráfico (datos) 
 MP / MC → MC: BR // (I) → E, S, M, O 
 MC → MC*: BC // (I), S, O(wr) → O 
 [ MC → MP: reempl. // M, O → (I) ] 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Tal ycomo se muestra en la tabla, comparado con el caso Firefly hay 
pocos cambios: se admiten varias copias de bloques sin actualizar entre 
diferentes copias, y por eso aparecen las transiciones M/S → O. 
 
Recuerda que MC → MC* representa la transmisión de una palabra de 
una cache a otra; esto es, una operación de broadcast para actualizar las 
copias del bloque. 
 
PW (BR) 
BR 
BC 
PR 
BR - BC 
 
 
BC 
PW PW sh (BC) 
PR - BR 
 
 
BR 
PR - PW 
PW 
PW 
PR 
M O 
E S 
nsh 
PR (BR) 
( - ) 
nsh 
sh 
sh 
 (BC) 
( - ) 
nsh sh 
 (BC) 
nsh 
3.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS SNOOPY ▪ 105 ▪ 
 
3.2.4 Resumen de los protocolos de tipo snoopy 
En los párrafos anteriores hemos presentado los principales protocolos de 
tipo snoopy, tanto los de invalidación como los de actualización. A pesar de 
que al principio surgieron muchos protocolos diferentes, hoy en día los 
principales son los que hemos comentado (o variantes de los mismos). 
Además, debido a ciertos inconvenientes en la implementación, los 
protocolos de actualización casi no se utilizan. Por tanto, los más utilizados 
son los protocolos de invalidación de 3 o 4 estados, en los que se utiliza la 
política de escritura write-back. 
 
3.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS 
SNOOPY 
3.3.1 Problemas 
Para mantener la coherencia de datos en un multiprocesador basado en un 
bus es suficiente un sistema de tipo snoopy. Los autómatas que hemos 
analizado (o variaciones de los mismos) son los que se utilizan en todos los 
multiprocesadores. La “lógica” que tienen que ejecutar los controladores de 
coherencia es bastante simple, tanto en el caso de invalidación como en el de 
actualización. Pero la implementación distribuida de esa lógica da lugar a 
nuevos problemas, que hacen que no sea inmediato conseguir dispositivos 
sencillos, eficientes y correctos. Por supuesto, el snoopy debe funcionar 
correctamente en cualquier situación, ya que de no ser así no podremos 
asegurar la coherencia de los datos y, por tanto, disponer de sistemas 
paralelos eficientes de memoria compartida. 
Un snoopy es un autómata distribuido que se ejecuta en P procesadores. 
Esto es lo que produce problemas, ya que hay que coordinar el 
funcionamiento de todos los controladores para, al final, obtener el resultado 
correcto. Dentro de los problemas que aparecen, la falta de atomicidad es, 
probablemente, el más importante: la necesidad de asegurar que no se 
mezclarán, en el tiempo, operaciones de coherencia de dos (o más) 
procesadores sobre un mismo bloque, produciendo resultados incorrectos. 
Para mostrar los problemas y las soluciones, analicemos cómo se organiza 
un controlador de coherencia para un caso “real”. En la figura se muestra el 
esquema de un controlador de coherencia (simplificado). Analicemos sus 
componentes principales. 
▪ 106 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3.3.1.1 Directorio de la memoria cache 
Las operaciones que se realizan en las memorias cache de los 
multiprocesadores pueden provenir de dos orígenes diferentes. Por un lado, 
de las acciones del propio procesador local; y por otro, de las acciones que 
aparecen en el bus compartido. Por tanto, tendremos interferencias entre 
ambas fuentes. Por ejemplo, ¿qué se debe hacer cuando se ve una señal INV 
en el bus, si en ese instante el procesador está utilizando la memoria cache? 
(o viceversa). 
Para conseguir un mejor rendimiento, normalmente el controlador de la 
cache se divide en dos partes: una analiza lo que está pasando por el bus 
(snoopy), y la otra procesa las peticiones del procesador. Ambas partes 
tienen que utilizar el directorio de la cache, y hacerlo con el menor número 
de interferencias posible: si el procesador está utilizando la cache, el snoopy 
se retrasará (y, como consecuencia, todas las transferencias de los demás 
procesadores); si es el snoopy el que está utilizando la cache, entonces será 
 
 
 
 
 
 
Cache data RAM 
 
tags + 
state 
snoopy 
 
Bus side 
controller 
 
compar. 
 
Processor 
side 
controller 
 
P 
 
compar. 
 
Cmd 
 
Addr 
 
Addr 
 
Cmd 
 
tags + 
state 
proc. 
 
Write-back buffer 
 
Data buffer 
 
tag 
 
state 
system bus 
to controller 
data 
to controller 
addr contr 
MC 
3.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS SNOOPY ▪ 107 ▪ 
 
el procesador el que tendrá que esperar. Por esto, normalmente, el directorio 
de la cache suele estar duplicado (o se utiliza una memoria de doble 
puerto), y cada parte del controlador utiliza su correspondiente directorio. De 
este modo, las operaciones (búsquedas, por ejemplo) se pueden hacer en 
paralelo en los dos directorios. Eso sí, los dos se deben mantener coherentes, 
es decir, si se realiza un cambio en uno de ellos, se debe de realizar también 
en el otro (si existen colisiones al hacer esto, una de las operaciones se 
deberá retrasar). Por suerte, las modificaciones —escrituras— de los 
directorios son mucho menos frecuentes que las lecturas. Los datos, por 
supuesto, no se duplican: ocupan mucho espacio y los controladores los 
utilizan con frecuencia mucho más baja. 
3.3.1.2 Búferes de escritura 
Cuando la política de escritura es write-through, hay momentos en los que 
se debe de actualizar un bloque de datos completo en memoria principal, 
bien porque se invalide o se reemplace (M → I, -), o bien para mantener la 
coherencia (M → S). Por ejemplo, supongamos que se debe reemplazar un 
bloque que está en estado M. Antes de traer el nuevo bloque, hay que 
guardar el viejo en la memoria, y esto implica un tiempo durante el cual el 
procesador está parado. Una mejora bastante común es hacer lo siguiente: en 
lugar de efectuar las dos operaciones en este orden <actualizar (BW) / leer el 
bloque nuevo (BR)>, se efectúan en el orden contrario: primero traer el 
bloque nuevo y, después, actualizar el bloque viejo en MP. Para poder hacer 
las operaciones en este orden, primero hay que realizar una copia del bloque 
en estado M que se quiere sustituir (si no, se perdería esa información); esta 
copia se hace en el búfer de escritura. Una vez que el bloque nuevo ya está 
en la cache y el procesador en marcha, se actualizará la MP con el bloque 
que está cargado en el búfer de escritura, normalmente aprovechando ciclos 
libres del bus. 
Esta mejora es común también en los sistemas de un solo procesador, pero 
en los multiprocesadores los búferes de escritura se deben de tratar con 
cuidado. Cuando un snoopy tiene que efectuar una búsqueda para saber si un 
determinado bloque está en la cache, además de en el directorio de la cache 
deberá buscar también en el/los búfer/es de escritura. Por tanto, el hardware 
de búsqueda, los comparadores, se debe duplicar: uno para el directorio de la 
cache y otro para cada búfer de escritura (figura anterior). 
 
▪ 108 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
3.3.1.3 Protocolo de petición de bus 
Cuando un procesador pone una petición en el bus, por ejemplo BR, debe 
esperar a la respuesta de los demás snoopy, por si existe alguna copia de ese 
bloque en otra cache: ¿se debe traer el bloque de memoria, o hay que traerlo 
de otra cache, que lo ha modificado? En ese caso, ¿cuánto tiempo hay que 
esperar hasta estar seguro de que todos han respondido? Las estrategias más 
utilizadas para controlar el tiempo de espera son las siguientes: 
• Esperar un tiempo fijo preestablecido, hasta estar seguros de que todos 
los snoopy ya han respondido. Por supuesto, es el caso peor, ya que la 
decisión se toma en el tiempo máximo (será el hardware del sistema el 
que determine ese tiempo), pero, a cambio, es el método más simple 
para implementar (Pentium quad/HP/SUN). 
• Esperar un tiempo variable (un handshake). Para reducir el tiempo de 
espera, se detecta cuándo responde el último snoopy, y la decisión se 
toma en ese momento. Así, en la mayoría de los casos la decisión se 
toma antes del tiempo máximo,pero es complejo de implementar, ya 
que se deben detectar y controlar las respuestas de todos los 
dispositivos. 
 Tanto en el primer caso como en el segundo, una optimización típica 
consiste en que, mientras se está esperando, se comienza con la lectura 
de memoria; y luego, dependiendo del caso, se aborta el acceso a 
memoria (si es que todavía no había terminado) o se bloquea la 
respuesta de la memoria hasta que todos los snoopy respondan (SGI 
challenge). 
• Añadir un bit más a todos los bloques de datos en memoria principal, 
para indicar si el bloque está en alguna cache o no. De esta manera, no 
hay que esperar a ninguna respuesta, ya que la conoceremos 
consultando ese bit. Esta solución es compleja, porque influye en 
todos los bloques de memoria principal, por lo que no se usa. 
 
Para poder aplicar estas estrategias necesitamos ayuda del hardware, 
normalmente más señales en el bus de control. Por un lado, la señal sh, que 
ya hemos utilizado, para saber si existen o no copias de un bloque de datos. 
Del mismo modo, es conveniente tener otra señal similar, dirty, para 
indicar si el bloque está modificado en alguna cache. Por último, es 
interesante tener otra tercera señal, inh (inhibir), para poder abortar los 
accesos a memoria principal. 
3.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS SNOOPY ▪ 109 ▪ 
 
3.3.1.4 Atomicidad: estado del controlador snoopy 
Para terminar con el análisis del controlador presentado en la figura 
anterior, nos falta un detalle: el estado del controlador. Tal y como hemos 
comentado al principio, uno de los principales problemas que se da cuando 
tenemos P procesadores ejecutando a la vez es el de la falta de atomicidad de 
las operaciones. Se dice que una operación es atómica si se ejecuta toda la 
operación, desde el comienzo hasta terminar, sin ningún tipo de interferencia 
de ningún otro procesador. 
El procedimiento para mantener la coherencia no es atómico por 
definición, ya que hay que realizar diversas operaciones y no se puede 
asegurar que no vaya a haber “interferencias” (no olvidar que tendremos 
muchos procesadores trabajando en paralelo). Dentro de ese conjunto de 
operaciones se encuentran las transferencias de datos por el bus. Vamos a 
suponer, por simplificar, que las operaciones del bus son atómicas; es decir, 
no se procesa otra petición hasta haber terminado con la anterior —no se 
segmenta19—. En los sistemas de un procesador, para trabajar con el bus se 
utilizan protocolos de comunicación similares a éste (por ejemplo, para una 
escritura): 
 
 procesador controlador del bus 
 
 petición-bus  
 ...  concesión-bus 
 dirección, control  
 ...  recibido 
 datos  
 
De esta manera, el controlador del bus establece orden y prioridades en el 
uso del mismo. En los multiprocesadores el control del bus es más 
complicado, por un lado porque hay muchos procesadores conectados al bus, 
y por otro porque los controladores de las caches son más complicados, para 
poder hacer las funciones del snoopy. Además, aunque ayuda, el hecho de 
que el bus sea atómico no asegura que el protocolo de coherencia lo sea. Es 
por tanto el propio protocolo quien tiene que asegurar la atomicidad de las 
operaciones. Analicemos cómo se puede conseguir atomicidad en un caso 
concreto, el protocolo Illinois (MESI). 
 
19 En los procesadores actuales esto no es así, ya que el uso de los buses está optimizado. 
▪ 110 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
3.3.2 El protocolo Illinois y la atomicidad 
3.3.2.1 Carreras: estados transitorios, señales BRQ y BGN 
El protocolo Illinois es un protocolo de invalidación de tipo MESI (lo que 
vamos a presentar se podría aplicar a los demás protocolos). Este protocolo 
utiliza la señal sh, para saber si los bloques están compartidos o no. 
Analicemos el siguiente caso. Dos procesadores comparten un bloque de 
datos, cuyas copias están en estado S. Los dos hacen una escritura a la vez en 
dicho bloque. ¿Cómo se resuelve el problema20? ¿Cómo asegurar que todas 
las operaciones que se deben hacer como consecuencia de esas escrituras se 
van a realizar del modo adecuado (incluso siendo el bus atómico)? 
Por ejemplo, los procesadores P1 y P2 envían la señal INV al bus. Uno de 
ellos ganará el uso del bus (supongamos que es P1). Por tanto, el controlador 
de coherencia de P2, en lugar de dejar el bloque en estado S (para luego 
ponerlo en estado M), lo pondrá en estado I (si no, el bloque estaría en los 
estados M y S en dos caches simultáneamente). Pero después de hacer esto, 
la señal enviada al bus, INV, no será suficiente, ya que ahora debería enviar 
también la señal BR. La consecuencia que podemos extraer está clara: el 
controlador del snoopy no se puede quedar esperando, sin hacer nada más, a 
la respuesta a su petición; tal vez tenga que cambiar la petición realizada si 
entre tanto otro procesador ha querido hacer una operación sobre el mismo 
bloque de datos. 
A este problema se le denomina "carrera" (race), y para solucionarlo, se 
suelen introducir más estados en el protocolo de coherencia, denominados 
estados "transitorios". Estos nuevos estados no están asociados a los 
bloques de la cache, sino al controlador de coherencia. Por tanto, no se 
introducen en el directorio (a nivel de bloque), sino que se guardan en un 
registro específico, en el mismo controlador (ver figura del controlador). Es 
decir, los posibles estados de un bloque son únicamente I, E, S y M. El 
significado de los estados transitorios es claro: algo se está haciendo, pero 
todavía no se ha terminado. 
Tal y como hemos comentado, vamos a suponer que las operaciones en el 
bus son atómicas, y para ello vamos a introducir dos señales de control en el 
protocolo: 
 
 
20 O, por ejemplo, el caso de dos escrituras simultáneas en fallo en dos procesadores. Los dos piden el 
bloque y, si en ese momento nadie dice que lo tiene (sh = 0), los dos lo colocarán en estado M. 
3.3 IMPLEMENTACIÓN DE LOS PROTOCOLOS SNOOPY ▪ 111 ▪ 
 
 - petición: BRQ (bus request) petición de utilización del bus 
 - respuesta: BGR (bus grant) permiso para utilizar el bus 
 
Para secuencializar operaciones que se quieren realizar simultáneamente, 
antes de utilizar el bus hay que efectuar una petición de uso (BRQ); cuando 
se dé permiso para utilizarlo (BGR), entonces se ejecutará el proceso 
correspondiente al protocolo de coherencia. 
Analicemos el protocolo Illinois teniendo en cuenta todo lo anterior. Para 
implementar un protocolo MESI, son suficientes 3 estados transitorios: ISE, 
IM y SM. El grafo del protocolo es el de la figura, y las principales 
transiciones entre los estados son las siguientes: 
 
• PR y fallo (I → S, E) 
 En lugar de ir directamente a E o S, se pasa al estado transitorio ISE. 
En la transición I → ISE se pide permiso para utilizar el bus (BRQ), y 
el controlador se mantendrá en ese estado hasta que se reciba el 
permiso (BGR). Cuando éste llegue, se pedirá el bloque (BR), y se 
cargará en la cache en el estado que corresponda, S o E, en función de 
la señal sh. 
 
• PW y fallo (I → M) 
 Antes de traer el bloque y modificarlo, hay que pedir permiso para 
usar el bus (BRQ), y mientras tanto se pasa al estado IM. Cuando 
llegue el permiso, se pedirá el bloque y se anulará el resto de copias 
(BR, INV); finalmente, se cargará el bloque en la cache en estado M. 
 
• PW y acierto (S → M) 
 Al igual que en los casos anteriores, pasaremos a un estado transitorio, 
a SM. Pero cuidado, el bloque estaba en estado S, y podría darse, a la 
vez, la misma transición en otra copia. Por tanto, mientras estamos en 
el estado transitorio SM, a la espera de poder utilizar el bus (para 
poder invalidar el resto de las copias), pueden suceder dos cosas: 
 
- Llega la señal de aceptación BGR; por tanto, el bloque pasará a 
estado M, y se generará la señal INV. 
- Se detecta la señal INV enel bus, lo que significa que otro snoopy 
se nos ha adelantado y quiere hacer una escritura sobre ese bloque. 
Debemos invalidar nuestra copia, por lo que el autómata pasará al 
▪ 112 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
estado IM, ya que ahora la escritura que queremos hacer partirá del 
estado I (es un fallo, por lo que hay que conseguir el bloque de 
datos: BR, INV). 
 
• PW y acierto (M, E → M) 
 En este caso no tendremos ningún problema; como nuestra copia es la 
única, se escribe y se modifica el estado, si es que estaba en E. 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3.3.2.2 Deadlock, livelock, starvation 
Los problemas comentados hasta ahora no son los únicos que se dan 
cuando se implementan protocolos de este tipo. El interbloqueo es otro de 
los problemas típicos. En el campo de las comunicaciones, el interbloqueo 
está relacionado con la ocupación de los buses; en los protocolos de 
coherencia, en cambio, puede aparecer otro tipo de interbloqueo: el 
denominado fetch deadlock. Veamos un ejemplo. 
El controlador de coherencia del procesador P1 está en un estado 
transitorio, esperando la respuesta del controlador del bus (y nada más). 
INV (BW) 
BR 
INV 
BR (BW) BGR (BR,INV) 
INV 
PW (BRQ) 
INV 
 
PR (BRQ) 
BGR (INV) 
PW (BRQ) 
PR - PW 
PR 
PW 
PR - BR 
 
IM 
 
ISE 
 
SM 
 
M 
 
I, - 
 
S 
 
E 
nsh sh 
BGR (BR) 
 
 ▪ 113 ▪ 
 
Mientras tanto, el controlador del procesador P2, que ha conseguido el bus, 
ha ejecutado la operación BR; por desgracia, el bloque que él quiere lo tiene 
P1, y además, en estado M. Consecuencia: el procesador P1 no le enviará el 
bloque, porque está esperando la señal BGR, y el procesador P2 no cederá el 
bus, porque el bloque que necesita es el de P1. El sistema se ha bloqueado. 
Por tanto, para evitar ese tipo de problema los autómatas de los snoopy no 
pueden dejar de espiar el bus ni en los estados transitorios. Si estando en un 
estado transitorio observa una situación como la descrita en el ejemplo 
anterior, deberá dar la respuesta adecuada. 
Al igual que sucede en otros contextos (por ejemplo, en la comunicación 
entre procesos), además del interbloqueo existen otros problemas, entre ellos 
los denominados livelock y starvation. El problema de livelock indica que se 
ha llegado a una situación en la que los procesos no están bloqueados 
("muertos / dead"), pero, sin embargo, son incapaces de avanzar. Por 
ejemplo, dos procesadores escriben a la vez sobre un mismo bloque que no 
tienen; los dos traerán el bloque e invalidarán el resto de las copias; en este 
caso se producirá livelock si la secuencia de acciones es la siguiente: rd1 – 
rd2 – INV1 – INV2 >> rd1 – rd2 – INV1 – INV2... Esto es, la operación 
no se va a terminar nunca. Por su parte, el problema de starvation suele 
aparecer ligado a cuestiones de prioridades: por ejemplo, un procesador 
nunca recibe respuesta a su petición del acceso al bus, porque siempre se le 
adelantan los demás. En el ejemplo de protocolo que acabamos de analizar 
estos dos problemas están resueltos. 
En resumen, los protocolos de coherencia se deben de diseñar con mucho 
cuidado, para evitar todo ese tipo de problemas y para que funcionen bien y 
de manera eficiente en cualquier situación. Al tratarse de protocolos 
distribuidos entre P procesadores, cumplir con esas características puede 
resultar complejo. 
 
3.4 SNOOPY JERÁRQUICO 
Para llevar a cabo la comunicación entre los procesadores de un 
multiprocesador hemos utilizado un bus. Como ya sabemos, el número de 
procesadores que se pueden conectar en un bus es limitado, y éste es el 
principal inconveniente de la utilización de un bus como red de 
comunicación. Pronto analizaremos más formas (redes) de conectar los 
procesadores, pero en este momento vamos a analizar otra red de 
▪ 114 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
interconexión, que es una evolución natural del bus: la jerarquía de buses. 
Un bus jerárquico es un árbol de buses, en el que en las hojas están los 
multiprocesadores (unidos mediante un bus) y en los demás "nodos" no hay 
más que buses y controladores. Estos últimos se encargan de gestionar la 
información por toda la red. Como vemos en la siguiente figura, se organiza 
un tipo de cluster en el que los nodos son pequeños sistemas SMP. 
 
 
 
 
 
 
 
 
 
Supongamos que tenemos una jerarquía de dos niveles, en la que el bus 
del segundo nivel se utiliza para conectar N multiprocesadores (cada uno con 
P procesadores en un bus). Aunque la memoria es compartida, lo más 
apropiado es distribuir físicamente la memoria, y de esta manera se llega a 
un sistema NUMA (non-uniform memory access): el tiempo de acceso es 
diferente en función de dónde esté situada la posición de memoria a la que se 
quiere acceder; no es por tanto, un sistema SMP. Dentro de cada 
multiprocesador SMP, se utiliza un protocolo snoopy para mantener la 
coherencia. Pero, ¿cómo mantener la coherencia en todo el sistema? 
Tal y como veremos más adelante, la solución que se utiliza en los 
sistemas que no utilizan una red de interconexión centralizada (un bus o 
similar) son los directorios de coherencia. Cuando se utiliza una jerarquía de 
buses, se utilizan unos controladores snoopy especiales que hacen la función 
de directorios, espiando y conectando dos niveles de bus, y decidiendo si hay 
que pasar la información de un nivel al otro o no. 
Estos "monitores" especiales para la coherencia deben de espiar dos tipos 
de operaciones: por un lado, las operaciones que se realizan sobre bloques de 
su memoria principal local que han sido copiados en una cache remota; y por 
otro lado, las que se hacen sobre bloques remotos que han sido traídos a las 
caches locales. Por supuesto, la información que se queda dentro de un nodo 
concreto (MP y cache) no afecta a los demás nodos, y será el snoopy local el 
que se encargue de mantener la coherencia. 
P 
C 
hardware para la 
coherencia global 
 
MP MP K K 
snoopy local 
 
SMP 
 
B1 
B2 
3.4 SNOOPY JERÁRQUICO ▪ 115 ▪ 
 
Vamos a dividir el monitor de coherencia o directorio en dos partes: 
• KL: controlador de coherencia que guarda información referente a los 
bloques locales que se encuentran copiados en memorias cache 
remotas (solamente los estados, no los datos, ya que el número de 
bloques que pueden estar "fuera" puede ser muy grande). 
• KR: controlador de coherencia que guarda información de los bloques 
remotos que se encuentran en las caches locales (una "cache" que 
guarda datos y estados, aunque con los estados sería suficiente; si 
están los datos, se reduce el tráfico en el bus, pero se aumenta la 
necesidad de memoria). 
¿Cómo funciona este hardware para mantener la coherencia? Veamos 
algunos ejemplos. 
3.4.1 Lecturas (fallo) 
En una cache se produce un fallo en lectura. Por tanto, se genera la señal 
BR en el bus B1. Existen dos posibilidades: 
 
1. La referencia pertenece al espacio de direccionamiento local 
 
• No hay copias fuera del nodo (por tanto, KL no responde): es una 
operación común y se resuelve dentro del mismo nodo (mediante el 
snoopy local). 
• Existe una copia de ese bloque fuera del nodo (por tanto, KL 
responde): 
- En estado S: no hay problema, se toma el bloque de su MP (o de 
otra MC local). 
- En estado E: es una situación similar a la anterior, pero el 
controlador KL tiene que avisar al otro nodo (al que tiene una 
copia del bloque), utilizando el bus B2, para que ponga el bloque 
en estado S (mensajes (a) y (b) de la figura). 
- En estado M: ¡cuidado! se debe pedir el dato fuera del nodo. La 
petición se pondrá en el bus B2. Cuando el controlador KR del 
nodo que tiene la copia del bloque detecte la petición realizará las 
siguientes acciones: (i) avisará a la cache local utilizando el bus 
B1, para que pase el bloque de estado M a estado S; y (ii) enviará 
▪ 116 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
el bloque a quien losolicitó (si tiene los datos en KR, desde ahí 
mismo; si no, buscará en qué cache local se encuentra el bloque). 
 Por último, el controlador KL que ha generado la petición tomará 
el bloque de datos del bus B2, y lo pondrá en el bus B1, para 
cargarlo en la memoria cache que corresponda y actualizar la 
memoria principal (en la figura: 1, 2, 3, 4 y 5). 
 
 
 
 
 
 
 
 
 
 
 
2. La referencia es del espacio de direccionamiento remoto. 
 
• KR no responde. Por tanto, no está en alguna otra cache del nodo. La 
petición se pasa al bus B2. El controlador KL correspondiente a esa 
dirección detectará la petición y la pondrá en el bus local. La respuesta 
(el bloque) llegará de la MP o de alguna de las caches locales de ese 
nodo, y se pasará el bus B2. Junto a ello, se actualizará la información 
de los controladores KL y KR. 
• KR responde. El bloque está en alguna de las caches locales y se 
tomará de ahí (si está en estado S, no hay que hacer nada; si está en 
estado E, se debe poner en estado S y hay que avisar al controlador 
KL; si está en estado M, además de lo anterior habrá que actualizar la 
MP). 
3.4.2 Escrituras 
Veamos un ejemplo concreto. El procesador P0 del nodo N1 quiere 
ejecutar una operación de escritura ST A en un bloque que está en estado S. 
MP 
MC 
MP 
KL KR KL 
B1 B1 
B2 
MC MC MC 
a 
b 
M→S 
M→S 
E→S 
E→S 
E→S 
M→S 
rd, fallo 
BR @ 
KR 
5 
3 
2 
4 1 
I→S 
@ → 
3.4 SNOOPY JERÁRQUICO ▪ 117 ▪ 
 
La variable A pertenece al espacio de direccionamiento del nodo N3, y hay 
una copia de dicho bloque en el nodo N2 en estado S. La operación se 
desarrollará de la siguiente manera: 
 
1. Se pone el bloque en estado M y se genera una señal de invalidación 
(INV) en el bus B1. 
2. El controlador KR del nodo N1 ve que es una referencia remota; por 
tanto, pasa la señal INV al bus B2. 
3. El controlador KR de N2 invalida su copia y pasa la señal INV al bus 
B1 (con lo que se invalidarán todas las copias de ese bloque que haya 
en ese nodo). 
3´. El controlador KL de N3 modifica el estado del bloque, de S a M. 
 
 
 
 
 
 
 
 
 
 
 
 
En general, cuando la memoria es compartida pero está físicamente 
distribuida no es sencillo mantener la coherencia de los datos. Los 
controladores de coherencia son dispositivos complejos y de gran tamaño, y, 
lo que es peor, la latencia de las operaciones de coherencia puede llegar a ser 
muy elevada, sobre todo si tenemos que acceder a datos fuera del nodo local. 
Y no podemos olvidar que hay que mantener la atomicidad de las 
operaciones de coherencia. 
Lo anterior ha sido simplemente un ejemplo. Normalmente, en lugar de 
utilizar jerarquías de buses se utilizan otro tipo de redes (por ejemplo, 
mallas), en las que no se pueden utilizar estrategias de tipo snoopy para 
mantener la coherencia. Por tanto, deberemos buscar otro tipo de solución al 
problema de la coherencia: el directorio, tal como veremos en el capítulo 7. 
MP 
MC 
KL KR 
MP 
KL KR 
MP 
KL KR 
S→M 
1 
2 
3 
3´ 
 
MC MC MC MC MC 
A 
N1 N2 N3 
B1 B1 
B2 
B1 
S→I S→I 
S→I 
S→I S→M 
INV A 
INV A 
INV A INV A 
INV A INV A 
S→M 
wr A 
 
▪ 4 ▪ 
 
 
Sincronización de Procesos 
en los Computadores SMP 
 
 
 
 
 
 
 
 
 
 
 
4.1 INTRODUCCIÓN 
En una máquina MIMD, la ejecución de los programas se divide en P 
procesos o hilos, que se ejecutan en paralelo en los procesadores del sistema. 
En general, la ejecución de esos procesos no es completamente 
independiente, sino que se comunican entre ellos, bien sea para pasarse datos 
o para sincronizar su ejecución. En este capítulo vamos a analizar los 
necesidades de sincronización entre procesos que se ejecutan en paralelo en 
una máquina SMP de P procesadores. 
▪ 120 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
Para presentar el problema de la sincronización entre procesos podemos 
utilizar un ejemplo muy sencillo. Supongamos que se va a ejecutar este 
código, en paralelo, en dos procesadores, P1 y P2 (inicialmente, A = 0)21: 
 
P1 
 
... 
ST A,F1 
... 
... 
P2 
 
... 
... 
... 
LD F4,A 
 
¿Qué valor leerá el procesador P2 en la variable A? Se trata de una 
variable compartida, que se utiliza en ambos procesos, y por tanto se debe 
mantener la coherencia (mediante un snoopy), lo cual implica que los 
cambios que efectúe P1 terminarán apareciendo en P2; sin embargo, no 
sabemos cuándo ocurrirá eso. 
En todo caso, el significado del programa anterior es confuso. ¿Existe una 
dependencia de datos (RAW) entre P1 y P2 en la variable A? Si es así, se 
debería indicar de alguna manera que P2 debe leer A después de que la haya 
modificado P1, y no antes. Algo similar debería ocurrir si existiera una 
antidependencia en A, para que P2 leyera A antes de que la modificara P1. 
En otras palabras, se necesita sincronizar el uso de la variable A para que el 
programa anterior tenga un sentido “lógico”. En general, en estos casos se 
utiliza la sincronización por eventos, para avisar a un proceso (consumidor) 
que se ha generado un dato en otro proceso (productor). 
La necesidad de sincronización no se reduce a casos como el anterior. 
Veamos otro ejemplo. Dos procesos comparten una variable, CONT, que 
hace las veces de contador. Ambos procesos incrementan el valor de dicho 
contador: CONT := CONT + 1. 
 
P1 
 
... 
LD R1,CONT 
ADDI R1,R1,#1 
ST CONT,R1 
... 
P2 
 
... 
LD R1,CONT 
ADDI R1,R1,#1 
ST CONT,R1 
... 
 
¿Qué valor tendrá la variable CONT tras ejecutar el código anterior en 
ambos procesadores? Aunque no existan problemas de coherencia, el 
 
21 Para simplificar el código, en los ejemplos de este capítulo utilizaremos el modo de direccionamiento 
absoluto. Como es habitual, el contenido del registro R0 es siempre 0. 
4.1 INTRODUCCIÓN ▪ 121 ▪ 
 
resultado no está claro. Por ejemplo, ambos procesos ejecutan “a la vez” el 
código citado, siendo CONT = 0, pero las instrucciones en cada procesador se 
intercalan en el tiempo de la siguiente manera: 
 
LD (P1) - ADDI (P1) - - ST (P1) 
 LD (P2) - - ADDI (P2) - ST (P2) 
 
El resultado es inesperado: aunque ambos procesadores han incrementado 
el valor de CONT, el valor final será CONT = 1. ¿Dónde está el problema? La 
variable compartida CONT se ha accedido de manera no adecuada, 
habiéndose intercalado en el tiempo las operaciones de P1 y P2 sobre dicha 
variable. ¿Cuál sería la solución? También en este caso se necesita 
sincronizar el uso de la variable compartida y ordenar su acceso (primero en 
un procesador y luego en el otro), para que el resultado de la ejecución en 
paralelo sea el esperado. De hecho, aunque el código se ejecute en dos 
procesadores, ese trozo de código se debería ejecutar en serie. Dicho de otra 
manera, la ejecución de ese código debe ser atómica. 
En el ejemplo anterior, los dos procesos sólo comparten una variable, 
sobre la que se efectúa una operación muy simple (+1), pero en general se 
ejecutan más operaciones sobre las variables compartidas. Por eso, algunos 
trozos de código de los procesos paralelos tienen que definirse como 
secciones críticas, y hay que controlar de manera adecuada que sólo un 
proceso ejecute simultáneamente dicho código, para lo que suelen utilizarse 
variables de tipo cerrojo, que funcionan como semáforos a la entrada de las 
secciones críticas, regulando el acceso de los procesadores a las mismas. 
En resumen, para poder ejecutar un programa en P procesadores, a 
menudo es necesario sincronizar el uso de las variables compartidas. La 
sincronización entre procesos puede resolverse por software o por hardware. 
Si se hace en hardware, suele ser más rápida pero menos flexible; si se hace 
por software (bibliotecas), se suelen obtener soluciones más flexibles. Hoy 
en día se utiliza una mezcla de ambos tipos; por una parte, se añaden 
instrucciones especiales al lenguaje máquina, y, por otra, utilizando esas 
instrucciones se escriben diferentes funciones de sincronización.Las estrategias básicas de sincronización son dos: exclusión mutua 
(mediante funciones lock/unlock) y sincronización por eventos (punto 
a punto, mediante indicadores o flags, o global, mediante barreras). En las 
operaciones de sincronización, los procesos esperan hasta que ocurra una 
determinada acción (que se abra el cerrojo, que se active un flag...). Como 
▪ 122 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
sabemos, los algoritmos de espera pueden ser de dos tipos: espera activa o 
bloqueo. En espera activa, el proceso entra en un bucle en el que 
continuamente se pregunta si ya se ha producido una determinada acción; 
mientras tanto, el procesador no realiza ninguna tarea útil. En los casos de 
bloqueo, en cambio, el sistema operativo efectúa un cambio de contexto para 
pasar a ejecutar otro proceso. El propio sistema operativo se encargará de 
“despertar” al proceso que está en espera cuando se produzca el evento 
esperado (o el propio proceso volverá cada cierto tiempo a analizar el estado 
de la sincronización). Ambos mecanismos, espera activa y bloqueo, son 
adecuados, y escogeremos uno u otro en función de las circunstancias 
concretas de la aplicación y de la máquina (tiempo a esperar, latencia del 
cambio de contexto, existencia de otros hilos o threads para ejecutar...); 
también puede utilizarse un sistema mixto: un tiempo umbral de espera, 
seguido de un cambio de contexto. En los ejemplos que vamos a analizar, 
utilizaremos un bucle de espera activa. 
¿De quién es la responsabilidad de escribir las rutinas de sincronización? 
En general, el programador utilizará las rutinas de sincronización de la 
librería del sistema (ya optimizadas); en todo caso, hay que analizar con 
detenimiento el comportamiento de dichas rutinas, porque no todas ellas son 
adecuadas para cualquier situación, situación que puede variar mucho de 
programa a programa o dentro del mismo. Por ejemplo, hay que dar solución 
eficiente al caso de un único procesador que desea entrar en una sección 
crítica o al caso de P peticiones simultáneas de entrada. Una función de 
sincronización que dé buen resultado en el primer caso, tal vez no lo dé en el 
segundo. 
Como hemos comentado, la sincronización no es algo intrínseco al 
algoritmo que se va a ejecutar, sino al hecho mismo de que se quiere ejecutar 
en paralelo, en P procesadores, lo que va a generar un tráfico de control 
específico. Por ello, un mecanismo de sincronización adecuado debe cumplir 
algunas condiciones, entre las que cabe destacar: 
 
• Baja latencia: se debe gastar el menor tiempo posible en efectuar la 
operación de sincronización, sea cual fuera la situación del programa; 
por ejemplo, no se debería perder tiempo en el cerrojo de una sección 
crítica cuando ésta está libre y no hay competencia en la entrada. 
• Tráfico limitado: el tráfico que se genera en el acceso y uso de las 
variables de sincronización debe ser el mínimo posible, para evitar 
saturar la red de comunicación. 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 123 ▪ 
 
• Buena escalabilidad: tanto la latencia como el tráfico no deben crecer 
(al menos no demasiado) con el número de procesadores del sistema. 
• Poco coste de memoria: no se debe utilizar mucha memoria para la 
sincronización. 
• Igualdad de oportunidades: todos los procesos deben tener las 
mismas oportunidades de resolver sus peticiones de sincronización; 
deben evitarse situaciones en las que, por ejemplo, un determinado 
proceso no consiga nunca entrar en una sección crítica, mientras que 
otros lo hacen una y otra vez (starvation). 
Definido el problema, analicemos las principales estrategias de 
sincronización. 
 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) 
Se utiliza la exclusión mutua para controlar la ejecución de un trozo de 
código que, aunque está replicado en P procesos, no puede ser ejecutado por 
más de un proceso simultáneamente. Ese trozo de código forma una sección 
crítica y nunca debe haber más de un proceso ejecutándolo. Para proteger el 
acceso a una sección crítica se utilizan dos funciones específicas, lock y 
unlock, que manejan una variable de tipo cerrojo, y que hacen las veces de 
un semáforo. 
El cerrojo puede tomar dos valores: 0 y 1, Si el cerrojo vale 0 (abierto), no 
hay problema alguno para ejecutar la sección crítica; en cambio, si el cerrojo 
vale 1 (cerrado) hay que esperar, ya que otro proceso está ejecutando en ese 
momento la sección crítica. 
Dos funciones se ejecutan con la variable cerrojo. El proceso que entra en 
la sección crítica cierra el cerrojo (lock), y, al finalizar la ejecución de la 
sección crítica, lo abre (unlock). Antes de entrar en la sección crítica, los 
procesos analizan el valor del cerrojo y se quedan a la espera mientras esté 
cerrado. Mediante esas dos funciones es posible gestionar el acceso a una 
sección crítica para que los procesos la ejecuten siempre de uno a uno: 
 
 lock(…) 
 [ sección crítica ] 
 unlock(…) 
 
La exclusión mutua puede lograrse también por medio del hardware. Por 
ejemplo, se pueden dedicar algunas líneas del bus de control para utilizarse 
▪ 124 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
como variables cerrojo hardware (or-wired, como la señal sh). Sin embargo 
no se suele utilizar esa solución, sino que las funciones lock y unlock se 
implementan en software. Veamos cómo podrían escribirse esas dos 
funciones (CER es una variable tipo cerrojo): 
 
función lock(CER) 
 
lock: LD R1,CER 
 BNZ R1,lock ; saltar si no es 0 
 
 ADDI R2,R0,#1 ; R2 := 1 
 ST CER,R2 ; cerrar cerrojo 
 RET 
función unlock(CER) 
 
unlock: ST CER,R0 
 RET 
 
Antes de entrar en la sección crítica se lee el cerrojo. Si está cerrado (CER 
= 1), los procesos se quedan en el bucle, analizando una y otra vez el valor 
del cerrojo; si está abierto (CER = 0), se puede pasar a la sección crítica, 
cerrando previamente el cerrojo. Finalmente, al terminar de ejecutar el 
código de la sección crítica se ejecuta la función unlock para abrir el 
cerrojo (CER = 0). 
Sin embargo, aunque las rutinas anteriores podrían ser adecuadas en el 
caso de un sistema con un solo procesador (en función de cómo se reparta el 
tiempo de ejecución), no funcionan bien en un sistema multiprocesador. 
¿Cuál es el problema? El mismo que tiene la sección crítica, la falta de 
atomicidad. El uso (lectura / escritura) de la variable CER no es atómico, 
por lo que no se puede impedir que dos procesos pasen a la sección crítica. 
El problema reside en que no existe una unidad de control centralizada, 
puesto que los procesos van en paralelo de manera completamente 
independiente. 
Para poder gestionar secciones críticas necesitamos disponer de 
instrucciones atómicas de tipo RMW (read-modify-write) que permitan 
efectuar una operación de lectura y escritura sobre una variable (el cerrojo) 
en modo atómico. Mientras se está ejecutando una operación especial de este 
tipo, el controlador del sistema de memoria bloquea el acceso del resto de 
procesadores a esa variable. 
Existen diferentes instrucciones de tipo RMW, y todos los procesadores 
actuales disponen de una o varias de ellas en su juego de instrucciones, ya 
que todos ellos están pensados para ser utilizados en entornos 
multiprocesador de memoria compartida. Veamos las principales 
instrucciones atómicas RMW. 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 125 ▪ 
 
4.2.1 Instrucciones Test&Set y Swap 
Como primera opción para gestionar cerrojos, vamos a analizar dos 
instrucciones similares. En ambos casos se ejecuta una operación de tipo 
RMW: se lee una variable en memoria, se modifica, y se vuelve a escribir en 
memoria, sin ninguna interferencia (operación atómica). 
4.2.1.1 Instrucción Test&Set 
Es una instrucción atómica RMW, la más antigua, que realiza la siguiente 
operación: 
 
▪ T&S R1,CER R1 := MEM[CER]; MEM[CER] := 1; 
 
Es decir, se carga una variable en un registro y se escribe un 1 en dicha 
variable en memoria. 
Utilizandola instrucción T&S, las dos funciones de un cerrojo 
(cerrar/abrir) pueden hacerse así: 
 
lock: T&S R1,CER 
 BNZ R1,lock 
 
 RET 
unlock: ST CER,R0 
 RET 
 
La instrucción T&S asegura que sólo un proceso leerá CER = 0, ya que 
junto a ello, de manera atómica, se escribe un 1; por tanto, el resto de los 
procesos verá un 1 en dicha variable, y continuará en el bucle de espera. 
Al salir de la sección crítica hay que abrir el cerrojo, y para ello es 
suficiente con escribir un 0 en la variable CER, con una operación "estándar" 
de escritura, ya que en la sección crítica sólo hay un proceso. 
4.2.1.2 Instrucción Swap 
La instrucción Swap es similar a T&S, pero, en lugar de escribir una 
constante en memoria, escribe el contenido de un registro. Se trata por tanto 
de un intercambio atómico entre el contenido de un registro y una posición 
de memoria: 
 
▪ SWAP R1,CER R1 ↔ MEM[CER]; 
 
▪ 126 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
Para efectuar un cerrojo, basta con cargar previamente un 1 en el registro. 
Éstas serían las correspondientes rutinas lock y unlock: 
 
lock: ADDI R1,R0,#1 ; R1 := 1 
 
l1: SWAP R1,CER 
 BNZ R1,l1 
 
 RET 
 
unlock: ST CER,R0 
 RET 
4.2.1.3 Análisis del tráfico 
Tal como hemos visto, las instrucciones de tipo RMW permiten controlar 
el acceso a una sección crítica, pero tenemos que analizar si se hace de 
manera eficiente o no. Como hemos comentado, las funciones de 
sincronización deben ser de latencia baja y generar poco tráfico, todo ello, a 
ser posible, independiente del número de procesos/procesadores, y con un 
reparto equilibrado de los recursos entre los procesos. 
Sin embargo, no es eso lo que ocurre. Supongamos que se utiliza un 
protocolo tipo MESI (invalidación) para mantener la coherencia de los datos. 
Cada vez que un proceso ejecuta la instrucción T&S se produce una escritura 
sobre una variable compartida, el cerrojo. La variable cerrojo estará en 
estado S (shared) en la cache y, al ser una escritura, habrá que invalidar 
todas las copias para mantener la coherencia (snoopy). Esto no es un 
problema si somos el único proceso intentando acceder a la sección crítica; 
sin embargo, si en ese momento hay muchos procesos efectuando la misma 
operación, el próximo intento en todos los procesadores será un fallo en 
cache (se ha anulado la variable cerrojo). Todos los procesos, más o menos a 
la vez, pedirán el bloque de datos correspondiente, por lo que se generará un 
tráfico de datos muy alto en el bus, más alto cuanto mayor sea el número 
de procesos esperando entrar en la sección crítica. Como consecuencia, las 
latencias (el tiempo de respuesta) de dichas operaciones crecerán mucho. 
En la siguiente figura puede observarse una simulación de dicha situación. 
Al principio, el procesador P0 está en la sección crítica y otros cuatro 
procesadores esperan para entrar. P0 abandona la sección crítica y escribe 
CER = 0 (unlock), por lo que invalida todas las copias de dicha variable. 
Los otros cuatro procesos, a la vez, pedirán (BRQ) el bloque que contiene 
CER, para poder ejecutar T&S. Al ser una instrucción atómica, el controlador 
del bus sirve las peticiones de manera "ordenada" (FIFO en la figura). Para 
indicar la atomicidad, hemos puesto la ejecución de la instrucción T&S entre 
corchetes. 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 127 ▪ 
 
Simulación de la entrada a una sección crítica 
Sincronización: Test&Set (TS) 
BRQ = petición de bloque / x = invalidado / transmisión de un bloque de datos 
P0 C=0 INV 
P1 ? x [TS BRQ TS INV] x SECCIÓN CRÍTICA 
P2 ? x [TS BRQ. . . . . . . . . . . . TS INV] [TS. . . . x BRQ. . . . . . . . . TS INV] [TS. . . . x BRQ. . . . . . . . . . 
P3 ? x [TS BRQ. . . . . . . . . . . . . . . . . . . . . . . TS INV] [TS . . . x BRQ. . . . . . . . . . . TS INV] [TS. . . . x BRQ. . 
P4 ? x [TS BRQ. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TS INV] [TS. . . . . x BRQ. . . . . . . . . . TS INV] [TS. . 
 repetir y repetir 
Tráfico de datos (bloques) 
 Para que entre un procesador en la sec. cr. → P + (P – 1) × k veces 
 Al salir de la sección crítica → 1 
 
La conclusión de la simulación es clara: mientras la sección crítica se 
mantiene ocupada, los procesos que intentan entrar están anulando, una y 
otra vez, el bloque que contiene la variable cerrojo, lo que implica que hay 
que enviar una y otra vez ese bloque, generando un gran tráfico en el bus. No 
hay que olvidar que ese tráfico no corresponde al algoritmo que se ejecuta 
sino al hecho de ejecutarlo en paralelo. 
Así pues, aunque la función lock anterior formalmente funciona bien, y 
en situaciones de poca competencia no da problemas, pero cuando la 
competencia por entrar en la sección crítica es alta el proceso se degrada 
mucho, es decir, no es escalable. Pueden plantearse, sin embargo, algunas 
mejoras en el diseño de las rutinas de acceso a la sección crítica, intentando 
reducir el tráfico y la latencia. 
4.2.1.4 Procedimiento Test&Set with backoff 
La fuente del tráfico que se genera en el bus está en la instrucción T&S 
(que efectúa siempre una escritura). Por tanto, deberíamos limitar el número 
de veces que se ejecuta dicha instrucción. 
Una primera alternativa sería esperar un cierto tiempo entre dos 
operaciones de T&S: 
T&S – t. de espera – T&S – t. de espera – ... 
▪ 128 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
es decir, si no es posible entrar en la sección crítica en un momento 
determinado, no intentarlo una y otra vez, generando tráfico y sin poder 
entrar, sino esperar un cierto tiempo para aumentar la probabilidad de 
encontrar libre la sección crítica. 
El tiempo de espera entre intentos no debería ser muy alto, para evitar 
tener el proceso parado cuando ya se ha liberado la sección crítica, ni muy 
bajo, para no intentar entrar en vano. Es decir, hay que tomar un 
compromiso entre reducir el tráfico (tiempo alto) y no perder tiempo en 
balde (bajo). Diferentes experimentos muestran que suele ser adecuado 
utilizar un tiempo que crece de forma exponencial, del tipo ti = k ci (k y c, 
dos constantes; i, número de intentos realizados para entrar en la sección 
crítica: 0, 1, 2...), lo que genera la siguiente secuencia de tiempos de espera: 
 
 t0 = k t1 = k c t2 = k c2 ... (c > 1) 
 
A esta estrategia se le suele denominar Test&Set with backoff. Las rutinas 
de control del cerrojo pueden ser las siguientes: 
 
lock: T&S R1,CER 
 BNZ R1,esp 
 
 RET 
 
esp: CALL ESPERA(t1) ; t1 = tiempo de espera 
 [t1 := ...] ; calcular nuevo valor para t1 
 JMP lock 
 
 
unlock: ST CER,R0 
 RET 
 
 
4.2.1.5 Procedimiento Test-and-Test&Set 
Veamos una segunda alternativa para reducir el tráfico. Cada vez que se 
ejecuta T&S se escribe un 1 en memoria... aunque el contenido de la 
memoria sea precisamente 1. ¿Por qué escribir en todos los intentos de 
acceso a la sección crítica en la variable cerrojo, si no se va a modificar su 
contenido? 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 129 ▪ 
 
La idea es dividir la operación de sincronización en dos fases. En la 
primera parte, simplemente se analiza el contenido del cerrojo, y para ello 
basta con utilizar una instrucción de lectura estándar. Repetiremos esa 
operación todas las veces que sea necesario hasta encontrar el cerrojo 
abierto. En ese momento, se ejecuta una operación de T&S, intentando cerrar 
de manera atómica el cerrojo. Sólo un proceso lo logrará, y el resto volverá a 
la fase inicial, a leer el valor del cerrojo. Así pues, cuando se abra el cerrojo 
cada proceso sólo escribirá una vez. 
Los procesos que están intentando acceder a la sección crítica no generan 
tráfico en el bus mientras la sección crítica está ocupada. Las invalidaciones 
(y, por tanto, la necesidad de tener que traer bloques de datos) sólo ocurrirán 
en dos ocasiones: cuandouno de los procesos cierra el cerrojo (escribe un 1) 
y cuando el proceso que termina la ejecución en la sección crítica lo abre 
(escribe un 0). 
A esta estrategia de sincronización se le conoce como Test-and-Test&Set, 
y las rutinas de control de la variable cerrojo son las siguientes: 
 
lock: LD R1,CER ; fase de test 
 BNZ R1,lock 
 
 T&S R1,CER ; fase de test-and-set 
 BNZ R1,lock 
 
 RET 
 
unlock: ST CER,R0 
 RET 
 
En comparación con el uso simple de la instrucción T&S, cuando se utiliza 
el procedimiento Test-and-Test&Set el tráfico generado se reduce 
notablemente. La siguiente figura muestra una simulación de dicha estrategia 
de sincronización. Al inicio, todos los procesos están en la fase de test (LD). 
Al abrirse el cerrojo, todos los procesos solicitan el bloque de datos que 
contiene la variable cerrojo, ya que ha quedado invalidado en todas las 
caches. Todos verán que el cerrojo está abierto (CER = 0), y ejecutarán T&S 
(atómico), pero sólo uno logrará pasar a la sección crítica; el resto volverá a 
la fase de test, ya que encontrarán el cerrojo cerrado. 
 
▪ 130 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
Simulación de la entrada a una sección crítica 
Sincronización: Test-and-Test&Set 
BRQ = petición de bloque / x = invalidado / transmisión de un bloque de datos 
P0 C=0 INV 
P1 LD x BRQ LD [TS . . . . . . . . . TS INV] x SECCIÓN CRÍTICA 
P2 LD x BRQ. . . . . LD [TS . . . . . . . . . x BRQ TS INV] LD. . . . x BRQ. . . . . . . . . . LD . . . . . . . . . 
P3 LD x BRQ. . . . . . . . . . . . LD [TS . . . . x BRQ. . . . . . . . . . . . TS INV] LD . . . . x BRQ. . LD . . . . . 
P4 LD x BRQ. . . . . . . . . . . . . . LD [TS x BRQ. . . . . . . . . . . . . . . . . . . . . . . . . TS INV] LD . . . . . . . . . . . . . . 
Tráfico de datos (bloques) 
 Para que entre un procesador en la sec. cr. → P + (P – 1) + (P – 2) 
 Al salir de la sección crítica → 1 
 En total → 3P – 2 
 
 Para que entren P → 
2
3
2
)13()23(
2
1
PPPP
P
p
→
−
=−∑
=
 
 
El tráfico que se genera es de orden P2, siendo P el número de procesos 
que está intentando acceder simultáneamente a la sección crítica; no es por 
tanto muy escalable, por lo que el bus se saturará con facilidad al crecer P. 
Además, el tráfico se genera en momentos concretos; todos los procesos 
fallan a la vez en la cache, al abrirse el cerrojo, y solicitan a la vez el bloque 
de datos (en este segundo caso no sirven las estrategias de esperar un cierto 
tiempo, ya que sólo se ejecuta una vez la instrucción T&S). 
 
4.2.1.6 Resumen de características 
Como hemos visto, las funciones más simples de tipo T&S para controlar 
el acceso a una sección crítica generan mucho tráfico de sincronización 
cuando existe alta contención en el acceso a la sección crítica mientras ésta 
está ocupada. Pero por otra parte, resultan muy adecuadas en casos de baja 
contención: son muy simples, tienen una latencia muy pequeña (pocas 
instrucciones) y no generan tráfico. 
Se utiliza muy poca memoria, ya que basta con una variable, 
independientemente del número de procesos. Desde el punto de vista del 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 131 ▪ 
 
equilibrio en el reparto de recursos, no se establece ningún tipo de política de 
asignación y, por tanto, el tiempo de espera a recibir respuesta dependerá de 
los criterios de prioridad que utilice el controlador del bus (por ejemplo, si es 
FIFO, sabemos que el tiempo estará acotado). 
En resumen, un T&S simple es una estrategia adecuada únicamente 
cuando se sabe que la contención en la entrada a la sección crítica va a ser 
muy baja (o cuando el número de procesadores del sistema es muy pequeño). 
La estrategia T&S-BO tiene un comportamiento similar, aunque genera 
menos tráfico y es, por consiguiente, más escalable. 
Test-and-T&S es el mecanismo más adecuado de los tres. En situación de 
alta competencia, mantiene el tráfico bastante limitado; cuando la 
competencia es baja, presenta una latencia algo superior a la de los casos 
anteriores, ya que hay que ejecutar siempre las dos fases: LD [fase de test] y 
T&S [fase de test-and-set]. 
Una última cuestión relacionada con la función lock. ¿Es necesario 
llevar a la cache la variable cerrojo de la función Test&Set, o es mejor 
mantenerla siempre en memoria principal? Sabemos que es útil llevar a la 
cache las variables que vamos a utilizar, pero si fallamos continuamente en 
el acceso al cerrojo, porque nos lo invalidan continuamente, y tenemos que 
transferir el bloque de datos completo una y otra vez, tal vez sería más 
cómodo dejar el cerrojo en la memoria principal y no hacer copias. En todo 
caso, si lo dejamos en MP, todos los accesos a dicha variable serían a la 
memoria principal, a través del bus, con lo que la latencia en casos de baja 
contención sería mucho más alta. 
 
4.2.2 Instrucciones Load Locked / Store Conditional y 
Compare&Swap 
Acabamos de comentar el problema que presenta una función de lock 
basada en la estrategia Test-and-T&S: cuando ejecutan la instrucción T&S 
todos los procesos efectúan una escritura en la variable cerrojo, se invalidan 
entre todos ellos y se genera un hot spot de tráfico, ya que todos los procesos 
solicitan en un intervalo muy corto de tiempo el bloque de datos 
correspondiente. Que el tráfico sea muy alto no se puede aceptar cuando el 
número de procesadores crece, por lo que hay que intentar algo más para 
reducir dicho tráfico. 
▪ 132 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
4.2.2.1 Instrucciones LL y SC 
Cada vez es más habitual en los procesadores que las operaciones 
atómicas necesarias para la sincronización se repartan en dos instrucciones, 
que, usadas de manera adecuada, permiten realizar una operación atómica 
RMW sobre una variable. Además de las dos instrucciones, se utiliza un flag 
hardware para saber que la operación se ha ejecutado de manera atómica. 
Las dos instrucciones específicas para sincronización son: LL –Load Locked 
(o linked)– y SC –Store Conditional–. 
La instrucción LL efectúa una lectura en memoria, pero tiene un efecto 
lateral: en un registro (latch) especial que sólo se usa para sincronización (le 
llamaremos LSin) se guarda la dirección accedida y un flag, para indicar 
que se ha leído dicha posición en un modo especial. 
 
▪ LL R1,CER R1 := MEM[CER]; 
 LSin[dir] := CER; LSin[flag] := 1; 
 
La instrucción SC efectúa una escritura condicional en memoria, para lo 
que primero analiza el latch LSin. Si contiene la dirección que se quiere 
escribir y el flag está en 1, entonces efectúa la escritura en memoria y envía 
una señal especial de invalidación de dicho flag a todos los procesadores, 
que al recibirla pondrán a 0 el flag si está asociado a la dirección indicada. 
En cambio, si el flag está desactivado, entonces la escritura no se ejecuta. En 
ambos casos, se devuelve un código de control, normalmente en el registro 
que se quiere escribir, indicando si la escritura se ha ejecutado o no. 
 
▪ SC CER,R1 si (LSin[dir,flag] = CER,1) { 
 MEM[CER] := R1; 
 LSin[flag] := 0 (INV, todos) 
 } 
 R1 := 1/0 (se ha escrito / o no) 
 
Veamos cómo se utilizan ambas instrucciones para gestionar la entrada a 
una sección crítica. La operación se realiza en dos o tres pasos: 
 
1 Se lee la variable de sincronización (cerrojo) mediante la instrucción 
LL, con lo que se guarda la dirección accedida y se activa el flag de 
sincronización en el latch de sincronización. 
 
2 Si es necesario, se efectúa cálculo o se procesan variables. 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 133 ▪ 
 
3 Se intenta escribir en la variable cerrojo (normalmente los resultados 
del segundo paso), mediante la instrucción SC. Si el flag de 
sincronización asociado a la dirección del cerrojo está activado, se 
realiza la escritura y se anulan, además de la variablecerrojo, todos 
los flags del sistema correspondientes a la dirección de la variable que 
se escribe: la operación total [LL — SC] se ha realizado atómicamente, 
sin interferencias. 
 En cambio, si el flag está desactivado, no se realiza la escritura, ya que 
otro proceso ha efectuado una escritura en dicha variable (razón por la 
cual se ha anulado el flag que se activó con la instrucción LL). No se 
ha podido realizar atómicamente el par [LL — SC] y por ello hay que 
repetir todo el proceso. Como SC no ha escrito, no se produce ninguna 
invalidación, ni se genera, por tanto, tráfico alguno. 
 
En resumen, si SC termina bien, entonces el trozo de código [LL — SC] 
se ha ejecutado atómicamente (lo cual no quiere decir que las instrucciones 
entre LL y SC formen una sección crítica). 
Utilizando esas dos instrucciones, las rutinas lock y unlock quedan de 
la siguiente manera: 
 
lock: ADDI R2,R0,#1 ; R2 := 1 
 
l1: LL R1,CER ; examinar cerrojo 
 BNZ R1,l1 
 
 ... 
 
 SC CER,R2 ; intentar cerrar cerrojo 
 BZ R2,lock ; SC no ha escrito, repetir 
 RET 
 
 
unlock: ST CER,R0 
 RET 
 
Como ocurre en el caso Test-and-T&S, no se genera tráfico mientras 
estamos en el bucle de espera (LL), ya que sólo se hace una lectura. Las 
mejoras vienen en la segunda parte. La instrucción T&S siempre escribe en 
memoria, independientemente del valor del cerrojo; en cambio, la 
instrucción SC sólo escribe cuando el cerrojo está abierto (es decir, cuando 
nadie ha escrito en dicha variable desde que se ejecutó LL). Así pues, sólo se 
genera tráfico en el bus en dos casos: al entrar en la sección crítica (cuando 
SC cierra el cerrojo), y al salir de la misma (al abrir el cerrojo). 
▪ 134 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
Simulación de la entrada a una sección crítica 
Sincronización: LL / SC 
BRQ = petición de bloque / x = invalidado / transmisión de un bloque de datos 
P0 C=0 INV 
P1 LL x BRQ LL(1) [SC . . . . . .SC INV] SECCIÓN CRÍTICA 
P2 LL x BRQ. . . . . LL(1) [SC . . . . . . . (0)x BRQ SC] LL. . . . . . . . . . . . 
P3 LL x BRQ. . . . . . . . . . . . LL(1) [SC . . . (0)x BRQ. . . . . SC] LL . . . . . . . 
P4 LL x BRQ. . . . . . . . . . . . . . LL(1) [SC (0)x BRQ. . . . . . . . SC] LL . . . 
Tráfico de datos (bloques) 
 Para que entre un procesador en la sec. cr. → P + (P – 1) 
 Al salir de la sección crítica → 0 
 En total → 2P – 1 
 
 Para que entren P → 
2
1
)12( PP
P
p
∑
=
=− 
 
En la figura anterior se muestra una simulación de esta estrategia. Como 
puede observarse, sólo una instrucción SC logra escribir en memoria, la 
primera, ya que las demás encuentran desactivado el flag que activaron con 
la instrucción LL. El tráfico de datos, por tanto, es menor. En todo caso, el 
tráfico todavía es alto, y además no se aplica ningún tipo de gestión de las 
peticiones de entrada. Hay, por tanto, oportunidades para la mejora. 
 
4.2.2.2 Instrucción Compare&Swap 
Antes de estudiar posibles mejoras del procedimiento anterior, veamos 
otra alternativa en la línea de la anterior. Se trata de la instrucción 
Compare&Swap, que realiza la siguiente operación en modo atómico: 
 
▪ C&S R1,R2,CER si (R1 = MEM[CER]) entonces MEM[CER] ←→ R2 
 
En este caso, la escritura en memoria tampoco se realiza en todos los 
casos, sino sólo cuando se cumple la comparación. Lo que en la pareja 
LL/SC se consigue con la ayuda del hardware (flag), en este caso se logra 
mediante el flag estándar resultado de una comparación. 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 135 ▪ 
 
El código para controlar un cerrojo usando la instrucción C&S es el 
siguiente: 
 
lock: ADDI R2,R0,#1 ; R2 := 1 
 
l1: C&S R0,R2,CER ; no escribe siempre 
 BNZ R2,l1 ; R2 = 1 → no se ha escrito 
 
 RET 
 
unlock: ST CER,R0 
 RET 
 
La instrucción C&S es más "compleja" que las anteriores, puesto que 
utiliza dos registros y una posición de memoria (es decir, una operación de 
memoria con tres operandos). En muchas arquitecturas RISC no se utiliza 
ese formato, por lo que suele ser más habitual que se utilice la pareja LL/SC. 
 
4.2.2.3 Algunos problemas con las instrucciones LL/SC 
Las instrucciones LL/SC necesitan la ayuda del hardware para cumplir su 
función. Por una parte, cada procesador tiene que disponer de un registro 
especial y un flag asociado al mismo, y, por otra, se necesita la colaboración 
del controlador del bus. Cuando se ejecuta LL se guarda la dirección 
accedida en dicho registro y se activa el flag. A partir de ese momento, el 
controlador deberá espiar continuamente el bus para detectar si se produce 
una escritura en esa dirección en algún otro procesador, en cuyo caso tiene 
que borrar el flag. También hay que borrar el flag cuando se reemplaza el 
bloque que contiene la variable de sincronización o en los cambios de 
contexto. 
Al ir a ejecutar la instrucción SC se mira el flag. Si está activado, no hay 
problemas: se escribe en memoria y se envía una señal de control para borrar 
todos los flags asociados a dicha dirección en el resto de procesadores. Pero 
si está desactivado, no se efectúa la escritura y se devuelve el 
correspondiente código de “error”. Hay que implementar el protocolo con 
cuidado para evitar problemas de deadlock, livelock, y similares. Por 
ejemplo, tendríamos un caso de livelock (repetir continuamente un proceso 
sin llegar a completarlo nunca) si ocurriera esto: LL – SC (fallo) – LL – SC 
(fallo) - ... (por ejemplo, porque se reemplaza continuamente el bloque que 
contiene la variable cerrojo). 
▪ 136 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
Para evitar estos problemas es conveniente no aceptar reemplazos del 
bloque que contiene la variable de sincronización. ¿Cómo? Por un lado, no 
efectuar operaciones en memoria entre las instrucciones LL y SC, para no 
tener que cargar nuevos bloques de datos en la cache (y evitar así un posible 
reemplazo). 
Por otro lado, aunque no se acceda a datos en memoria, es muy probable 
que haya instrucciones entre LL y SC. Como normalmente la cache de datos 
y la de instrucciones estarán separadas, no tendremos problemas. En todo 
caso, se recomienda siempre utilizar muy pocas instrucciones entre la pareja 
LL/SC, para reducir la posibilidad de que otro procesador acceda a la 
variable de sincronización intercalándose con nosotros. 
4.2.3 Instrucciones Fetch&Op 
En muchos casos, las operaciones que se realizan con las variables 
compartidas son muy simples, tal como hemos visto en los ejemplos 
anteriores. Por ello, existen instrucciones especiales que realizan esas 
operaciones de modo atómico: las instrucciones Fetch&Op. Se trata de un 
grupo de instrucciones RMW de uso más general que las anteriores: antes de 
volver a escribir la variable en memoria se efectúa algún tipo de operación 
(op) con la misma. Según la operación que se realice, tenemos diferentes 
instrucciones; por ejemplo: 
 
▪ Fetch&Incr R1,VAR R1 := MEM[VAR]; 
 MEM[VAR] := MEM[VAR] + 1; 
 
▪ Fetch&Dcr R1,VAR R1 := MEM[VAR]; 
 MEM[VAR) := MEM[VAR] – 1; 
 
▪ Fetch&Add R1,R2,VAR R1 := MEM[VAR]; 
 MEM[VAR] := MEM[VAR] + R2; 
 
Por ejemplo, utilizando la instrucción Fetch&Incr podemos 
incrementar el valor de la variable CONT de manera atómica: 
 
 Fetch&Incr R1,CONT 
 
El valor de la variable CONT se deja en el registro R1, y, a la vez, se 
incrementa el contenido de la posición de memoria CONT. Es decir, si CONT 
valía 6, tras ejecutar la instrucción tendremos que R1 = 6 y CONT = 7. 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 137 ▪ 
 
Si el código que hay que ejecutar en exclusión mutua es más largo (algo 
más complejo que una simple operación de incremento), entonces habrá que 
generar una sección crítica; aunque utilizando este tipo de instrucciones 
también se pueden implementar dichas funciones, lo más habitual es utilizar 
otro tipo de instrucciones atómicaspara hacerlo. 
4.2.4 Alternativas para reducir el tráfico 
Tal como hemos comentado, con las instrucciones LL/SC conseguimos 
reducir el tráfico, pero aún caben ciertas optimizaciones. Analicemos las más 
importantes. 
4.2.4.1 Tickets 
Un mecanismo basado en tickets puede ser útil para reducir el tráfico en la 
entrada a una sección crítica. La idea es sencilla. Un proceso que quiere 
entrar en la sección crítica tiene que coger primero un ticket, que le indica el 
número de turno de entrada que le corresponde. A continuación, se quedará 
esperando a que llegue su turno. En ese momento, solamente él tendrá 
permiso para entrar en la sección crítica: es su turno. Al abandonar la 
sección crítica incrementará la variable que indica el turno, para dejar paso al 
siguiente proceso. 
Con el método de los tickets no se produce contención en la entrada de la 
sección crítica, ya que todas las entradas se han ordenado, y por tanto se 
reduce algo el tráfico. En cambio, hay que utilizar dos variables compartidas: 
la que sirve para repartir tickets (TICKET), y la variable que indica el turno 
actual (TURNO). 
El contador que se utiliza para repartir tickets tiene que accederse en 
exclusión mutua, para lo que podemos utilizar, si disponemos de ello, una 
instrucción de tipo Fetch&Incr o bien las instrucciones LL y SC. Por 
ejemplo: 
 
 F&I R1,TICKET ; R1 := MEM[TICKET]; 
 ; MEM[TICKET]:= MEM[TICKET]+ 1 
 
o bien: 
 
tick: LL R1,TICKET ; conseguir ticket 
 ADDI R2,R1,#1 ; incrementar número de ticket para el siguiente 
 SC TICKET,R2 ; pero de manera atómica 
 BZ R2,tick ; repetir la operación hasta conseguir atomicidad 
▪ 138 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
Utilizaremos una u otra solución en función del tipo de instrucciones que 
pueda usar el procesador. Finalmente, las rutinas de lock y unlock 
quedarán así22: 
 
lock: F&I R1,TICKET ; obtener ticket 
 
esp: LD R2,TURNO 
 SUB R3,R1,R2 
 BNZ R3,esp ; esperar turno 
 
 RET 
 
 
unlock: LD R1,TURNO ; actualizar turno 
 ADDI R1,R1,#1 
 ST TURNO,R1 ; para dar paso al siguiente 
 RET 
 
En la siguiente figura (un poco más adelante) se presenta una simulación 
de esta estrategia. Los procesos ya han conseguido su ticket y están 
esperando su turno. Sólo se genera tráfico una vez, cuando se incrementa el 
turno al salir de la sección crítica, aunque tendríamos que contar también el 
tráfico generado al obtener el ticket, ya que hay que traer a la cache el bloque 
que contiene dicha variable. Sumado todo el tráfico, el nivel del mismo es 
similar al que habíamos conseguido antes, aunque ahora está más distribuido 
en el tiempo (la obtención de los tickets no tiene por qué ser simultánea), lo 
que también es importante. 
A pesar de todo, todavía hay momentos en los que se genera bastante 
tráfico. Al actualizar la variable TURNO (al salir de la sección crítica) se 
producirán fallos en la cache en todos los procesadores que estén esperando 
entrar, que pedirán, más o menos a la vez, una copia de dicho bloque: se 
genera un “pulso” de tráfico. 
Desde el punto de vista de la latencia, cuando no se produce contención 
(simultaneidad) en la entrada, esta técnica es de latencia más alta, puesto que 
primero hay que conseguir el ticket. 
El reparto de peticiones es justo: se aplica una política tipo FIFO. Si se 
quiere, en este caso se pueden aplicar técnicas de espera tipo backoff, con un 
tiempo de espera “proporcional” a la diferencia entre el ticket obtenido y el 
turno actual. 
 
22 Si el número de procesos es P, conviene incrementar las variables TICKET y TURNO módulo P, para 
evitar desbordamientos. 
4.2 EXCLUSIÓN MUTUA (mutual exclusion) ▪ 139 ▪ 
 
4.2.4.2 Vectores de cerrojos 
Como hemos comentado, el método anterior genera momentos de tráfico 
alto al actualizar la variable TURNO, compartida por todos los procesos. El 
problema desaparece si, en lugar de obtener un ticket con el turno 
correspondiente, se obtiene la dirección de un elemento de un vector de 
cerrojos, un cerrojo particular donde esperar para entrar en la sección 
crítica. Así, primero se reparten posiciones del vector de cerrojos —valores 
de la variable INDICE—, tal como hemos hecho con los tickets en el 
método anterior; luego, cada proceso espera a que se abra su cerrojo 
particular: VECT_CER(INDICE). 
 
vector de cerrojos: VECT_CER → ... 0 1 1 1 1 ... 
 
 
 proceso en la sección crítica INDICE: siguiente posición de espera 
 
Las rutinas de lock y unlock serían las siguientes: 
 
lock: F&I R1,INDICE ; obtener posición del vector de cerrojos 
 ; ¡ojo! función módulo 
esp: LD R2,VECT_CER(R1) ; esperar turno 
 BNZ R2,esp 
 
 ST MI_INDICE,R1 ; guardar índice para la salida de la S.C. 
 RET 
 
 
unlock: ADDI R2,R0,#1 ; R2 := 1 
 
 LD R1,MI_INDICE ; recuperar índice del vector de cerrojos 
 ST VECT_CER(R1),R2 ; cerrar cerrojo propio (1) 
 
 ADDI R1,R1,#1 ; ¡ojo! función módulo 
 ST VECT_CER(R1),R0 ; abrir siguiente cerrojo (0) 
 RET 
 
En la figura siguiente se muestra una simulación del tráfico generado. El 
tráfico es ahora constante, independiente del número de procesos, porque al 
salir de la sección crítica sólo se actualiza (y se anula) el cerrojo de un 
proceso. El resto de procesos no se entera, y continúa a la espera de su turno. 
Estamos suponiendo que no existe un problema de falsa compartición en la 
cache, es decir, que los elementos del vector de cerrojos están en bloques 
diferentes de memoria. 
▪ 140 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
Simulación de la entrada a una sección crítica 
Sincronización: Tickets / Vectores de cerrojos 
BRQ = petición de bloque / x = invalidado / transmisión de un bloque de datos 
 Tickets Vectores de cerrojos 
P0 TURNO++ INV VC(i+1)= 0 INV 
P1 LD x BRQ LD SEC. CRIT. LD x BRQ LD SEC. CRIT. 
P2 LD x BRQ. . . . . LD . . . . . . . . . . . . . LD . . . . . . . . . 
P3 LD x BRQ. . . . . . . . . . . . LD . . . . . . . . LD . . . . . . . . . 
P4 LD x BRQ. . . . . . . . . . . . . . LD . . . LD . . . . . . . . . 
Tráfico de datos (bloques) TICK. V.C. 
 Para conseguir el ticket / turno → 1 1 
 Para que entre un procesador en la sec. cr. → P 1 
 Al salir de la sección crítica → 0 1 
 En total → P + 1 3 
 
 Para que entren P → P+3) / 2 3P 
 
El tráfico de datos se reduce considerablemente, pero en cambio se 
necesita más memoria para implementar la sincronización (un vector de P 
elementos). 
 
Como hemos visto, existen diferentes alternativas para gestionar secciones 
críticas (para generar funciones lock); por tanto el programador tendrá que 
analizar las características de su aplicación y de la máquina paralela para 
optar por la más adecuada. 
Como ejemplo final, y a modo de resumen, el tráfico que se generará en el 
bus, si tenemos P = 7 procesadores (en una máquina de 8) esperando a entrar 
en una sección crítica, será el siguiente en función de la estrategia empleada: 
 
T-T&S: P(3P–1) / 2 → 70 bloques LL/SC: P2 → 49 bloques 
Tick.: P(P+3) / 2 → 35 bloques V.C.: 3P → 21 bloques 
 
4.3 SINCRONIZACIÓN "PUNTO A PUNTO" MEDIANTE EVENTOS ▪ 141 ▪ 
 
4.3 SINCRONIZACIÓN "PUNTO A PUNTO" 
MEDIANTE EVENTOS 
Decimos que la sincronización es "punto a punto" si sólo toman parte en 
la misma dos procesadores (o grupos): el primero avisa al segundo de que se 
ha ejecutado determinada operación. La sincronización se suele ejecutar 
mediante un bucle de espera activa sobre una variable común que hace las 
veces de flag o indicador. 
El flag o indicador es una variable de control que permite sincronizar 
ambos procesos. Por ejemplo, en el caso de un productor y un consumidor, 
la sincronización puede ser así: 
 
P1 (productor)X = F1(Z); 
aviso = 1; 
P2 (consumidor) 
 
while (aviso==0) {}; 
Y = F2(X); 
 
(En algunos casos se puede usar el propio resultado como indicador; por ejemplo, si 
sabemos que el resultado va a estar en un rango determinado, el consumidor puede 
quedarse esperando mientras el resultado esté fuera de ese rango.) 
 
La idea anterior (un flag de sincronización) puede extenderse y ejecutarse 
en hardware (y así se ha intentado en algunas máquinas de tipo experimental 
y en situaciones de paralelismo de grano muy fino), añadiendo a cada 
posición de memoria un bit de control full/empty que indique si se ha escrito 
un nuevo dato desde la última vez que se leyó el anterior o no. La 
sincronización productor/consumidor se efectuaría de la siguiente manera: el 
productor escribe en la posición de memoria un nuevo dato si el bit de 
control asociado está a 0, y en ese caso lo pone a 1; el consumidor lee el 
contenido de la posición de memoria si el bit de control está a 1, y en ese 
caso lo pone a 0. No es una solución que se haya aplicado comercialmente, 
ya que es cara (1 bit por cada posición de memoria), requiere instrucciones 
especiales de memoria y presenta problemas en casos como, por ejemplo, un 
productor y muchos consumidores. 
 
La sincronización por eventos se efectúa mediante una escritura y un 
bucle de espera. En algunos contextos, esas dos operaciones se indican 
mediante dos funciones específicas. Por ejemplo: 
▪ 142 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
flag = 1 → post(flag) [ signal(flag) ] 
while (flag == 0) {} → wait(flag) 
 
Esas funciones pueden generalizarse utilizando vectores de flags (vectores 
de eventos): 
 
post(vf,i) → activar el elemento i del vector de flags: vf(i) := 1 
wait(vf,i) → esperar a que el elemento idel vector de flags vf sea 1 
 
4.4 SINCRONIZACIÓN MEDIANTE BARRERAS 
En la ejecución en paralelo de los programas suele ser muy habitual que 
se necesite sincronizar un grupo de procesos entre sí de manera global, todos 
a la vez; por ejemplo, para asegurar que todos los procesos han llegado a un 
determinado punto en la ejecución del programa. Para ese tipo de 
sincronización se utilizan barreras (barrier). 
Para construir una barrera de sincronización se utiliza una variable 
cerrojo, un contador y un flag. En la barrera se sincronizan P procesos. 
Cuando los procesos llegan a la barrera, incrementan el valor de un contador 
—en exclusión mutua— y se quedan esperando a que lleguen todos los 
procesos. Cuando llega el último, activa el indicador de barrera abierta, y 
todos los procesos abandonan la misma. Veamos algunos ejemplos. 
4.4.1 Una barrera sencilla 
El código siguiente representa una barrera de sincronización sencilla. Se 
ha definido un struct, de tipo tipo_barrera, con tres variables: un 
cerrojo, un contador y un flag para indicar el estado de la barrera, cerrada (0) 
o abierta (1). Además de ello, se utiliza la variable local mi_cont, que 
indica cuántos procesos han llegado a la barrera. 
 
struct tipo_barrera 
{ 
 int cer; variable para el cerrojo 
 int cont; núm. proc. que han llegado a la barrera 
 int estado; estado de la barrera 
}; 
 
struct tipo_barrera B; declaración de la barrera 
4.4 SINCRONIZACIÓN MEDIANTE BARRERAS ▪ 143 ▪ 
 
BARRERA (B,P) P = número de procesos 
{ 
 LOCK(B.cer); entro en la sección crítica 
 if (B.cont == 0) B.estado = 0; soy el primero, cierro la barrera 
 B.cont++; 
 mi_cont = B.cont; cuántos hemos llegado a la barrera 
 UNLOCK(B.cer); salgo de la sección crítica 
 
 if (mi_cont == P) soy el último 
 { 
 B.cont = 0; inicializo el contador 
 B.estado = 1; abro la barrera 
 } 
 else while (B.estado == 0) {}; espero hasta que la barrera se abra 
} 
 
Los procesos que ejecutan la barrera incrementan el valor de B.cont, 
dentro de una sección crítica. El primer proceso (B.cont = 0) cierra la 
barrera, tras lo cual todos los procesos que entran en la barrera pasan a 
esperar que la barrera se abra (B.estado = 1). El último proceso que llega 
a la barrera (B.cont = P) la abre, y, como consecuencia de ello, todos los 
procesos abandonan el bucle de espera. 
Tras incrementar el contador dentro de la sección crítica, se utiliza la 
variable mi_cont para decidir si hay que abrir la barrera o no. Dicha 
variable es necesaria porque, tal y como está escrito el código, no se puede 
utilizar, sin más, el contador B.cont, ya que en ese momento puede haber 
otro proceso en la sección crítica incrementando dicho contador. Si se quiere 
utilizar la variable B.cont, se debe mantener la sección crítica hasta 
después de la comparación de la instrucción if, y luego terminar la sección 
crítica (unlock) por las dos ramas del if (then y else). 
4.4.2 Barreras reutilizables 
¿Algún problema con la barrera anterior? Sí, si se utiliza de manera 
repetida (por ejemplo, dentro de un bucle): cálculo / barrera / cálculo / 
barrera..., lo cual es muy normal, ya que la barrera será normalmente una 
función de biblioteca que llamarán los procesos una y otra vez. 
Supongamos que se está ejecutando una barrera de sincronización. El 
último proceso entra en la barrera y la abre, para que todos los procesos 
▪ 144 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
salgan y continúen ejecutando el programa. Si en el código vuelve a aparecer 
una llamada a la barrera, es posible que un proceso entre en esa segunda 
llamada a la barrera cuando tal vez algún proceso no haya abandonado 
todavía la anterior, porque, por ejemplo, no se ha enterado todavía de que la 
barrera se ha abierto (no estaba en ejecución). 
El primer proceso (B.cont = 0) que vuelva a entrar en la barrera la 
cerrará (B.estado = 0) de nuevo. Por tanto, los procesos que se hayan 
quedado en la primera barrera ya no podrán salir, y los que entren en la 
segunda nunca llegarán a abrirla, porque no están todos. Claramente, hemos 
llegado a una situación de deadlock. 
¿Cómo evitar ese problema? Por ejemplo, 
 
a. Utilizando un contador que cuente el número de procesos que 
abandonan la barrera (de manera similar a como se hace al entrar). 
Mientras no la abandonan todos, ningún proceso puede volver a 
entrar. 
 Por una parte, la latencia de la barrera de sincronización puede ser 
mayor (en ocasiones hay que esperar, aunque no hay que olvidar que 
es el último proceso en llegar el que marca la latencia de la barrera), y, 
por otra, puede haber una mayor competencia en la entrada de la 
barrera (mientras se espera, se agrupan los procesos). 
 
b. Utilizando valores diferentes, de barrera a barrera, para indicar que la 
barrera está abierta (bit alternante, sense reversal). ¿Cuántos valores 
diferentes habría que utilizar? Es suficiente con dos, 0 y 1, puesto que 
no es posible tener más de dos instanciaciones simultáneas de la 
misma barrera. Así pues, el flag que abre la barrera irá alternando de 
valor de una a la siguiente. 
 Cada proceso utiliza una variable privada para saber el valor actual 
que indica que la barrera está abierta; no usamos por tanto una 
variable compartida como en el caso anterior (B.estado) para 
indicar el estado de la barrera. 
 
De acuerdo a esta segunda opción, la barrera quedaría así: 
 
 
4.4 SINCRONIZACIÓN MEDIANTE BARRERAS ▪ 145 ▪ 
 
 La variable val_sal es local, una por proceso, e indica el valor actual que permite 
salir de la barrera. 
 
BARRERA (B,P) 
{ 
 val_sal = !(val_sal); actualizar el valor del bit de apertura 
 
 LOCK(B.cer); 
 B.cont++; 
 mi_cont = B.cont; 
 UNLOCK(B.cer); 
 
 if (mi_cont == P) soy el último 
 { 
 B.cont = 0; inicializo el contador 
 B.estado = val_sal; abro la barrera 
 } 
 else while (B.estado != val_sal) { }; espero a que se abra la barrera 
} 
4.4.3 Eficiencia 
Los criterios de eficiencia de este tipo de sincronización son los mismos 
que en el caso anterior: la latencia debe ser baja (no hay que efectuar 
muchas operaciones para entrar en la barrera), tiene que generarse poco 
tráfico, debe escalar bien conel número de procesos, etc. 
En lo que al tráfico que se genera en una barrera de P procesos se refiere, 
podemos hacer la siguiente estimación. Supongamos que las variables de la 
barrera (cer, cont y estado) se encuentran en bloques diferentes (para 
evitar la falsa compartición). En general, el proceso Pi tiene que conseguir 
cuatro bloques de datos: el de la variable cer, para entrar en la sección 
crítica; el de la variable cont, para incrementar el contador; y el de la 
variable estado dos veces, para quedarse en el bucle de espera, y para salir 
del mismo, ya que ha sido anulado por el proceso que abre la barrera. Por 
tanto, el tráfico generado será del orden de 4P bloques (para ser más 
precisos, 4P – 2, ya que el primer y el último proceso necesitan un bloque 
menos cada uno). 
Analizado en el tiempo, el tráfico se va a repartir, en general, de la 
siguiente manera: 2 - 3 - 3... - 3 - P–1; es decir, el tráfico que se genera al 
entrar en la barrera suele estar repartido en el tiempo (suponiendo que no hay 
▪ 146 ▪ Capítulo 4: SINCRONIZACIÓN DE PROCESOS EN LOS COMPUTADORES SMP 
contención en la entrada a la barrera; si no, la función lock generará más 
tráfico, tal como hemos visto en los apartados anteriores), pero las últimas 
P–1 peticiones se generan a la vez, ya que todos los procesos (salvo el 
último) están esperando a salir de la barrera; en ese momento, por tanto, la 
latencia de servicio de los bloques será más alta. 
Como en los casos anteriores, también aquí son posibles algunas 
optimizaciones. El objetivo es reducir el número de procesos que acceden a 
la misma variable, y para ello puede montarse una estructura en árbol, 
binario por ejemplo (en un bus no se gana nada, ya que todas las 
trasferencias aparecen en el bus, pero sí cuando la red de comunicación es de 
otro tipo, no centralizada). En todo caso, el tipo de barreras que hemos visto 
funcionan suficientemente bien en los sistemas SMP, y no es necesario, 
salvo casos muy particulares, otro tipo de estructura. 
Las barreras pueden implementarse también en hardware, si se dispone de 
un bus específico de control. La implementación es similar al caso de la 
línea de control sh (AND wired). 
 
 
4.5 RESUMEN 
Es habitual tener que sincronizar la ejecución de procesos que se ejecutan 
en paralelo, para que el uso de las variables compartidas sea el adecuado. En 
algunos casos, hay que utilizar secciones críticas, para lo que se utilizan unas 
instrucciones específicas del lenguaje máquina del procesador que se pueden 
ejecutar de manera atómica. De esta manera, se puede leer, modificar y 
escribir una variable sin que interfiera ningún otro proceso (atómicamente). 
En otros casos, la sincronización hay que implementarla mediante eventos, 
punto a punto, o hay que sincronizar un conjunto de procesos, mediante 
barreras. 
Las diferentes instrucciones atómicas que se utilizan para operaciones de 
sincronización son “similares”, y en un procesador concreto sólo tendremos 
una o algunas de ellas. De todos modos, se puede “simular” el 
comportamiento de unas mediante otras. Por ejemplo, se puede escribir una 
rutina que simule el comportamiento de un F&I o de un T&S mediante las 
instrucciones LL y SC. Pero no hay que olvidar que algunas pueden ser más 
adecuadas que otras, en función del tráfico que generan (los bloques de datos 
que se invalidan en las escrituras). Por ejemplo, una función lock 
implementada simplemente mediante la instrucción T&S es adecuada si no 
4.5 RESUMEN ▪ 147 ▪ 
 
hay contención (competencia) para entrar en la sección crítica, pero no es 
nada eficiente si se espera una elevada contención. 
En los sistemas SMP el tráfico es un problema importante, y, por tanto, se 
han desarrollado diferentes estrategias para reducir el tráfico que generan las 
funciones de sincronización: backoff, test-and-test&set, tickets, vectores de 
cerrojos... Por tanto, es responsabilidad del programador seleccionar entre 
las diferentes alternativas la más adecuada para su aplicación, bien sea 
utilizando funciones de una biblioteca del sistema, o bien sea escribiendo 
funciones específicas. 
 
 
▪ 5 ▪ 
 
 
Consistencia de la Memoria en 
los Computadores Paralelos 
 
 
 
 
 
 
 
 
5.1 INTRODUCCIÓN 
5.1.1 Sistemas de un solo procesador 
¿En qué orden se ejecutan las instrucciones en un procesador? La 
pregunta tiene más interés del que parece, y tal vez sin pensar mucho 
podríamos responder: en el orden en que están en el programa. Sin embargo, 
sabemos que eso no es verdad. Aunque el modelo de ejecución sigue siendo 
von Neumann, se aplican muchas optimizaciones, tanto hardware como 
software, que implican cambios en el orden de las instrucciones del 
programa. Por ejemplo, la ejecución de las instrucciones está segmentada y 
el inicio y final de las instrucciones no respeta el orden original (modelos 
orden/desorden tipo scoreboard o Tomasulo, búferes de instrucciones en los 
superescalares, etc.). Por parte del software, sabemos que el compilador 
▪ 150 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
puede desordenar las instrucciones para obtener ejecuciones más eficientes 
(list scheduling, trace scheduling, software pipelining...). 
Lo más preocupante de esas reordenaciones está en las instrucciones de 
memoria. La memoria del computador debería mantener en todo momento el 
"estado actual" de la aplicación; es decir, debería reflejar en todo momento 
hasta qué punto se ha llegado en la ejecución de la misma y los resultados 
obtenidos. Pero sabemos que no es eso lo que ocurre. Por ejemplo, al utilizar 
la memoria cache no se mantiene actualizada en todo momento la memoria 
principal; admitimos que las instrucciones LD adelanten a las ST en 
ejecución; se utilizan búferes de escritura para volcar contenidos de cache a 
memoria principal y no parar al procesador; el propio compilador puede 
eliminar algunos accesos a memoria y usar en su lugar datos de los registros 
(por ejemplo, al desenrollar bucles con recurrencias), etc. De hecho, es en 
ese tipo de optimizaciones donde se encuentra una de las razones del 
aumento de velocidad de los procesadores actuales. 
Así pues, la ejecución de un programa sigue su propio camino con el 
objeto de lograr la mayor eficiencia posible. En todo caso, la ejecución del 
programa debe ofrecer siempre exactamente los mismos resultados que si se 
ejecutara en orden estricto. Por ejemplo cualquier lectura de memoria debe 
obtener siempre lo escrito en dicha variable la última vez. 
En los sistemas con un único procesador, todas las optimizaciones que 
hemos comentado están bajo control de la única unidad de control del 
sistema, y pueden llevarse así a buen puerto. No ocurre lo mismo, en 
cambio, en los sistemas multiprocesador, donde el control de los procesos en 
ejecución es esencialmente distribuido. 
5.1.2 Sistemas multiprocesador 
Lo que está resuelto en los sistemas de un procesador, se convierte en un 
problema grave en los multiprocesadores, al estar el control descentralizado 
entre todos los procesadores. De hecho, ¿en qué orden se ejecutan las 
instrucciones en un sistema paralelo, considerándolo en su totalidad? ¿es el 
resultado correcto en todos los casos? 
Si la comunicación entre procesos se lleva a cabo en memoria principal, 
por ejemplo, las cuestiones anteriores se reducen a esta otra: ¿en qué orden 
se ejecutan las instrucciones de memoria en un sistema paralelo? 
5.1 INTRODUCCIÓN ▪ 151 ▪ 
 
Al problema que hace referencia al orden de ejecución de las 
instrucciones, y, en general, a la imagen que tienen los procesadores del 
sistema de memoria se le conoce como el problema de la consistencia. El 
problema de coherencia de los datos que hemos analizado en el capítulo 3 
también hace referencia a estas cuestiones, pero de manera más limitada. 
Recordemos que un protocolo de coherencia asegura que: 
• los cambios que se efectúan en una variable en una determinada cache 
aparecerán en algúnmomento en todas las caches. 
• los cambios que se efectúan en una variable aparecen en el mismo 
orden en todos los procesadores. 
Así pues, al mantener la coherencia de los datos del sistema aseguramos 
que todos los procesadores van a observar todos los cambios que se 
produzcan en las variables compartidas. Sin embargo, no sabemos nada 
sobre el orden en que se verán los cambios producidos en variables 
diferentes. 
5.1.3 Semántica de los programas y orden de 
ejecución de las instrucciones 
Antes de nada, es necesario estar seguros de la semántica de los 
programas que se ejecutan en paralelo, entendidos como un todo, para evitar 
que los resultados obtenidos nos sorprendan, para lo que es necesario 
controlar adecuadamente el uso de las variables compartidas. El orden de 
ejecución de las instrucciones es especialmente importante para entender el 
comportamiento de un programa paralelo. Por ejemplo, ¿cuál será el 
resultado en P2 al ejecutar este programa paralelo (inicialmente, A = B = 0)? 
¿Tiene un significado claro el programa? 
 
P1 P2 
 
A = 1; (wr1) 
B = 2; (wr2) 
 
print B; (rd1) 
print A; (rd2) 
 
 
Tenemos cuatro combinaciones de resultados posibles: BA = 00, 01, 21 y 
20. Según el orden en que se intercalen las instrucciones a lo largo del 
tiempo, las tres primeras posibilidades pueden interpretarse correctamente. 
Por ejemplo, P2 imprimirá BA = 01 si se ejecutan las instrucciones en este 
orden en el tiempo: 
▪ 152 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
P1 P2 
 
A = 1; 
... 
B = 2; 
... 
 
... 
print B; 
... 
print A; 
 
 
En cambio, la cuarta combinación, BA = 20, parece “imposible”. Si B = 2, 
entonces A debería ser siempre 1. Sin embargo, esa combinación es posible 
si el control de P2 decide desordenar la ejecución de sus instrucciones (el 
modelo de ejecución habitual es desorden/desorden): dos lecturas en 
memoria pero sobre variables diferentes, es decir, completamente 
independientes desde su punto de vista. Por tanto, tal como está escrito, el 
programa anterior resulta muy ambiguo. 
Como hemos analizado en el capítulo anterior, la semántica de los 
programas paralelos se asegura normalmente mediante operaciones de 
sincronización; de esa manera, en el siguiente ejemplo deberíamos obtener 
siempre A = 1 en P2. 
 
P1 P2 
 
A = 1; (wr1) 
LISTO = 1; (wr2) 
 
while (LISTO == 0) {}; (rd1) 
print A; (rd2) 
 
 
A pesar de ello, podemos seguir teniendo problemas con el orden de las 
instrucciones. Las dependencias de datos del programa deberían asegurar el 
resultado correcto, pero, desgraciadamente, esas dependencias se producen 
entre programas (procesadores) diferentes, y no dentro del mismo programa. 
 
wr1 (A) rd1 (LISTO) 
wr2 (LISTO) rd2 (A) 
 
Si se respeta el orden de las instrucciones en cada procesador, es decir, el 
orden entre lecturas y escrituras en memoria (wr1 >> wr2; rd1 >> rd223), 
entonces se respetará también la dependencia wr1 → rd2, ya que tenemos 
que: wr1 >> wr2 → rd1 >> rd2 ⇒ wr1 → rd2. Pero si no (si el compila-
dor o el hardware desordenan el código), podría ser que obtuviéramos A = 0. 
 
23 Utilizamos el símbolo >> para indicar orden entre operaciones: A >> B indica que A debe ejecutarse 
antes que B. El símbolo → indica una dependencia de datos: A → B indica que el dato producido por 
A se utiliza en B. 
tiempo 
5.1 INTRODUCCIÓN ▪ 153 ▪ 
 
El problema de la ordenación de las instrucciones se observa también en 
este otro ejemplo (inicialmente, F1 = F2 = 0): 
 
P1 P2 
 
F1 = 1; 
if (F2 == 0) then 
 < código > 
... 
 
F2 = 1; 
if (F1 == 0) then 
 < código > 
... 
 
 
Las dependencias entre la instrucciones son las siguientes: 
 
wr1 (F1) wr2 (F2) 
rd1 (F2) rd1 (F1) 
 
De acuerdo a la lógica secuencial, no es posible que ambos procesadores 
ejecuten el código de la rama then, ya que hay que respetar 
obligatoriamente la ordenación wr1 >> rd1 y wr2 >> rd2; pero si se 
desordena el código en cada procesador (para lo que no hay problema 
alguno, ya que no hay dependencias entre las instrucciones), es posible que 
ambos procesadores pasen a ejecutar dicho código. 
Los cambios de orden que hemos citado son muy habituales en los 
sistemas de un solo procesador; más aún, son imprescindibles para lograr un 
rendimiento adecuado del sistema. 
5.1.4 Atomicidad de las instrucciones 
Las operaciones de memoria son atómicas si mientras se efectúan no se 
realiza ninguna otra operación en memoria. Además, la finalización de la 
operación debe entenderse en su sentido más amplio, incluyendo los efectos 
de la operación en el resto de los procesadores (por ejemplo, una escritura no 
termina hasta que se han anulado todas las copias de ese bloque en el 
sistema). Veamos un ejemplo. 
 
P1 P2 
 
A = 1; (wr1) 
LISTO = 1; (wr2) 
 
while (LISTO == 0) {}; (rd1) 
print A; (rd2) 
 
 
▪ 154 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
Aunque se mantenga el orden de las instrucciones, seguimos teniendo un 
problema. El protocolo de coherencia nos asegura que los cambios 
efectuados en P1 llegarán a P2, pero nada nos dice sobre el orden en que 
llegarán, ya que se trata de dos variables diferentes24 Si el nuevo valor de 
LISTO llega a P2 antes que el de A, entonces se imprimirá A = 0. Esto puede 
ocurrir si las escrituras en memoria de P1 no son atómicas, y se continua con 
la ejecución del programa (wr2) antes de que finalice “por completo” (las 
consecuencias) la instrucción anterior. En definitiva: necesitamos saber 
cuándo finaliza una instrucción de memoria antes de poder empezar con otra. 
El problema es incluso más general. En este ejemplo también aparece la 
necesidad de atomicidad (A = B = 0): 
 
P1 P2 P3 
 
A = 1; 
 
while (A == 0) {}; 
B = 1; 
 
 
while (B == 0) {}; 
C = A; 
 
Cuando P1 escribe en A, el nuevo valor aparecerá en algún momento en 
P2 y P3. Al llegar a P2 se ejecutará B = 1, cuyo nuevo valor también llegará 
a P3. ¿Cuál de los dos cambios se hará efectivo en primer lugar en P3? Si el 
primero es el cambio producido en P2, entonces es posible que finalmente se 
ejecute en P3 C = 0 y no C = 1. 
La atomicidad de las operaciones de memoria es un problema global, y 
debe mantenerse en cada procesador y en el sistema global. 
5.1.5 Modelos de consistencia 
El sistema paralelo debe ofrecer exactamente los mismos resultados que el 
de un solo procesador al ejecutar un determinado programa, es decir, debe 
ser consistente. Como hemos visto en los ejemplos anteriores, el problema 
corresponde a las operaciones de memoria (principalmente en el acceso a 
variables compartidas), debido a los cambios de orden y a la falta de 
atomicidad. 
 
24 Los “mensajes / señales de control” enviados de un procesador a otro pueden llegar al destino en 
desorden, en función de la red y de los protocolos de comunicación. Eso es muy claro en los sistemas 
de memoria distribuida, pero también puede darse en los sistemas SMP (con bus) en función del tipo 
de bus y del protocolo de comunicación. 
5.2 CONSISTENCIA SECUENCIAL (SC, sequential consistency) ▪ 155 ▪ 
 
Tanto los programadores de software del sistema como los de aplicaciones 
necesitan un modelo que especifique el orden y la atomicidad de las 
instrucciones que se ejecutan en paralelo, para saber qué optimizaciones 
pueden hacerse en el código y para poder interpretar adecuadamente el 
comportamiento de los programas. Un modelo de consistencia debe definir 
un espacio de memoria “coherente” para todos los procesadores, 
especificando las relaciones de orden que van cumplir las operaciones de 
memoria. En los próximos apartados vamos a presentar los principales 
modelos de consistencia, primeramente el modelo de consistencia 
secuencial, y luego los modelos relajados. 
 
 
5.2 CONSISTENCIA SECUENCIAL (SC, sequential consistency) 
Como ya hemos comentado, la consistenciano es un problema grave en 
los sistemas de un solo procesador: sólo existe un flujo de instrucciones y el 
orden de las instrucciones está bajo control. El compilador puede efectuar 
cambios en el orden de las instrucciones (por ejemplo, adelantar lecturas), y 
disponemos de hardware para parar el procesador y resolver las 
dependencias de datos. 
El modelo de consistencia secuencial (SC) consiste en extender al 
multiprocesador el modelo de orden estricto de un procesador. Un 
multiprocesador es secuencialmente consistente si: (a) se mantiene el orden 
local de las instrucciones en cada procesador y (b) el orden de las 
instrucciones en todo el sistema (global) corresponde a un determinado 
entrelazado de las instrucciones de cada procesador. 
El modelo SC es el que normalmente espera un programador, el más 
intuitivo. El modelo impone dos condiciones: 
 
1. Hay que mantener el orden local en cada procesador. 
 Esto implica que no se pueden desordenar las instrucciones LD y ST. 
Hay que mantener, por tanto, las cuatro relaciones de orden siguientes, 
para cualquier dirección y en todos los procesadores: 
 
 wr >> rd; wr >> wr; rd >> rd; rd >> wr. 
 
 Por ejemplo, los dos primeros casos del siguiente ejemplo respetan el 
modelo SC, mientras que los otros dos no, porque no se mantiene el 
orden local (operaciones de memoria). 
▪ 156 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
P1 P2 En conjunto SC si SC no 
a 
b 
c 
d 
a a a c 
b c d d 
c b c b 
d d b a 
 
 
2. También hay que mantener el orden global, por lo que no puede 
ejecutarse ninguna instrucción de memoria hasta que finalice la 
anterior de cualquier procesador (y todas sus consecuencias). Para 
poder asegurar esta condición es necesario que las operaciones de 
memoria sean atómicas (write atomicity = todas las escrituras, en 
cualquier posición, deben aparecer en el mismo orden en cualquier 
procesador). No se admite, por tanto, esta situación: 
 
 
 
En los ejemplos anteriores hemos puesto de manifiesto la necesidad de 
estas condiciones. Así pues, con el modelo SC el problema de la consistencia 
desaparece de raíz, ya que se impone un orden estricto, tanto local como 
global, a todas las operaciones de memoria, junto con la atomicidad de 
dichas operaciones. El modelo de memoria es por tanto de orden estricto, y 
los programas paralelos se comportarán “tal como se espera”. En la figura 
aparece un esquema lógico de la estructura que impone este modelo. 
 
 
 
 
 
 
5.2.1 Orden y atomicidad de las instrucciones de 
memoria 
Imponer orden local en un procesador es relativamente sencillo, al menos 
si sabemos cuándo ha terminado la operación anterior. La atomicidad en 
cambio es más complicada, dado que cada procesador utiliza una cache 
local. Para cumplir con el modelo SC se debe hacer lo siguiente: 
tiempo 
instrucción a 
instrucción b 
P P P P 
 
MEM 
orden 
atomicidad 
5.2 CONSISTENCIA SECUENCIAL (SC, sequential consistency) ▪ 157 ▪ 
 
1. Hay que mantener el orden de las instrucciones LD y ST en cada 
procesador (sencillo de cumplir, ya que el orden de esas instrucciones 
corresponde a una sola unidad de control, la de cada procesador). 
 
2. Además del orden, para poder asegurar la atomicidad, hay que esperar 
a que finalicen las operaciones de memoria de cada procesador antes 
de poder ejecutar la siguiente. El final de una lectura (LD) es simple 
de detectar: cuando se reciben los datos. Con las escrituras, en cambio, 
el problema es más complejo. Cuando se ejecuta un ST, el procesador 
no puede ejecutar otra instrucción de memoria hasta que la escritura y 
sus consecuencias (invalidación o actualización de las copias) en 
todos los procesadores se hayan ejecutado (write completion). 
 Para asegurar que se han efectuado todas las invalidaciones o 
actualizaciones es necesario complicar el protocolo de coherencia, 
añadiendo respuestas a dichas acciones: señales o mensajes de 
“confirmación” tipo ACK (acknowledgement). Tras actualizar sus 
copias, cada procesador “envía” un mensaje de ese tipo; al recibirse 
todos los mensajes, la operación se da por finalizada (problema: 
¿cómo saber cuántas copias hay?) 
 
 
 
3. Para asegurar la atomicidad hay que cumplir dos condiciones: 
(a) Por un lado, los cambios en una variable han de verse en el 
mismo orden en todos los procesadores. Por ejemplo, 
 
P1 P2 P3 P4 
 
A = 2; 
B = 1; 
 
 
A = 3; 
C = 1; 
 
 
while (B ≠ 1) {}; 
while (C ≠ 1) {}; 
reg1 = A; 
 
while (B ≠ 1) {}; 
while (C ≠ 1) {}; 
reg2 = A; 
 
 
 Si los cambios en A (2 y 3) llegan en distinto orden a P3 y P4, 
entonces el sistema no será consistente, ya que se asignarán 
valores diferentes a reg1 y reg2, con lo que la escritura de A no 
habrá sido atómica. 
 Cuando la red de comunicación del multiprocesador es un bus, el 
propio protocolo de coherencia (el snoopy) y una gestión 
adecuada del uso del bus permiten asegurar el orden de las 
escrituras. Como vamos a ver en un próximo capítulo, cuando se 
1. INV 
2. ACK 2. ACK 
1. INV 
▪ 158 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
utilizan otro tipo de redes más generales, hay que utilizar 
directorios para mantener la coherencia de los datos y el orden de 
las escrituras. 
 
(b) Y por otra parte, antes de ejecutar una operación de lectura hay 
que esperar a que finalice totalmente la última operación de 
escritura que se ejecutó sobre dicha variable (en general, en otro 
procesador), así como todas sus consecuencias. 
 Si el protocolo de coherencia es de invalidación, esta operación no 
es complicada: nuestra copia está invalidada, y por tanto tenemos 
que pedir una nueva copia, que recibiremos cuando haya 
terminado la operación. En cambio, si las copias se actualizan, el 
proceso de coherencia se vuelve más complejo: tras enviar la 
señal de actualización de la variable (1), hay que esperar a recibir 
todas las confirmaciones (2), tras lo cual hay que enviar una nueva 
señal indicando que ya se puede utilizar dicha variable (3), con lo 
que finaliza la operación de escritura. La complejidad del 
protocolo de actualización es la razón por la que no se suelen 
utilizar protocolos de este tipo. 
 
 
 
5.2.2 Efectos en el hardware y en el compilador 
Las condiciones que hemos impuesto para mantener la consistencia, orden 
y atomicidad, son muy “fuertes”: se complica el uso de la memoria cache y 
además no se pueden aplicar las optimizaciones más habituales en el caso de 
un solo procesador. Recuerda que mientras se efectúa una operación de 
memoria en un procesador, nadie puede efectuar otra operación en memoria, 
y las instrucciones de memoria vienen a representar un 25% - 35% del total. 
Por ejemplo, debido a la necesidad de asegurar la atomicidad, no se 
pueden utilizar búferes de escritura, ya que ello supone en definitiva la 
posibilidad de adelantar las lecturas. De la misma manera, el compilador no 
puede efectuar las reordenaciones de código típicas, si con ello se modifica 
el orden de las operaciones de memoria. Y tampoco puede optimizarse el uso 
de la memoria mediante la utilización de registros, (para ahorrarnos 
operaciones LD/ST). Por ejemplo, esta optimización no funciona en un 
multiprocesador: 
1. BC 1. BC 
2. ACK 2. ACK 
3. seguir 3. seguir 
5.3 MODELOS RELAJADOS (relaxed) ▪ 159 ▪ 
 
P1 P2 P1 P2 
A = 1; 
B = A; 
 
 
(2 ST / 1 LD) 
 
A = 0; 
 
 
 r1 = 1; 
A = r1; 
B = r1; 
 
(2 ST) 
A = 0; 
 
 
La variable B puede tomar los valores 0 o 1 en el programa original; en el 
segundo, en cambio, nunca se producirá el caso B = 0, ya que se ha 
eliminado la lectura de A (básicamente, un adelanto del LD). No es posible 
esa optimización en un modelo de consistencia secuencial. 
Dado que el modelo impone condiciones muy restrictivas, podemos 
intentar no cumplir con alguna de ellas en determinados casos. Por ejemplo, 
podemos intentar ejecutar las instrucciones LD en modo especulativo, antes 
de que haya finalizado el anteriorST. Si finalmente todo va bien, seguiremos 
adelante sin más cuidados; pero si el bloque se ha anulado o actualizado en 
el camino, habrá que echar marcha atrás (roll-back, de manera similar a 
como se hace con las apuestas en los saltos). En todo caso, como vamos a 
ver, es posible mantener la consistencia del sistema en muchas situaciones 
sin utilizar tantas limitaciones. 
 
5.3 MODELOS RELAJADOS (relaxed) 
El conjunto de condiciones que impone en el modelo SC es suficiente para 
asegurar la consistencia, pero no estrictamente necesario. Además, desde el 
punto de vista de la eficiencia, las repercusiones sobre el sistema son 
grandes, al impedir muchas optimizaciones y obligar a esperar a la 
finalización global de las operaciones de memoria antes de comenzar una 
nueva. Por ello, y de cara a mejorar la eficiencia del sistema, se han 
propuesto varios modelos de consistencia más flexibles, en los que se 
eliminan algunas de las restricciones anteriores. 
Analicemos las necesidades de orden de manera más fina. El orden de las 
instrucciones de memoria se reduce a estos cuatro casos: 
 
 rd >> rd25 rd >> wr 26 wr >> rd wr >> wr 
 
 
25 Considerando que las caches no se bloquean en los fallos 
26 Cuidado con los tres casos siguientes: si es la misma dirección, estamos ante un caso de dependencia 
de datos. 
▪ 160 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
El modelo de consistencia secuencial impone el orden en los cuatro casos. 
Los modelos relajados, en cambio, permiten que no se respete alguno de 
ellos. Para definir un modelo de consistencia relajado hay que indicar: 
 
• qué orden se respeta y cuál no entre las instrucciones de memoria. 
• si se cumple o no la atomicidad de las escrituras en memoria (ST), lo 
que permitiría efectuar una lectura aunque no hayan concluido los 
efectos de la escritura anterior en todos los procesadores. 
 
En todo caso, cuando se utiliza un modelo de consistencia relajado, tiene 
que existir siempre la posibilidad de dejar en suspenso las optimizaciones e 
imponer el orden estricto. Para ello se suelen utilizar nuevas instrucciones 
máquina del procesador (normalmente a través de funciones de biblioteca 
del sistema). Estas instrucciones se denominan barreras de ordenación 
(fence), y se utilizan como puntos de control. Una instrucción de este tipo 
impone un determinado orden en las instrucciones de memoria y asegura que 
las instrucciones posteriores no comienzan hasta que no hayan finalizado 
todas las anteriores. 
Las instrucciones concretas tipo fence dependen del procesador en 
particular, y pueden llamarse MEMBAR, STBAR, SYNC... En general suelen 
ser de alguno de los siguientes tres tipos: 
• Write-fence: para asegurar que todas las escrituras (ST) anteriores ha 
finalizado en todo el sistema antes de que comience ninguna escritura 
posterior (es decir, para imponer el orden wr >> wr). 
• Read-fence: misma función que la anterior, pero con las lecturas (se 
utilizan normalmente para evitar el adelantamiento de los LD). 
• Memory-fence: misma función, pero para ambas operaciones, rd y 
wr. 
 
Por definición, si el modelo de consistencia es el secuencial, entonces 
todas las operaciones de memoria se tratan como instrucciones tipo fence. 
5.3.1 Total Store Ordering (TSO) / Processor 
Consistency (PC) 
El objetivo de esta primera optimización es “esconder” la latencia de las 
escrituras en memoria, y para ello se admite que se ejecute una instrucción 
LD aunque no haya finalizado un ST anterior; es decir, se permite el 
5.3 MODELOS RELAJADOS (relaxed) ▪ 161 ▪ 
 
adelantamiento de los LD: no se asegura el orden wr >> rd. La única 
diferencia entre los modelos TSO y PC es que en el caso del modelo 
Processor Consistency no se asegura que las operaciones de memoria sean 
atómicas. 
El esquema de memoria correspondiente sería, esquemáticamente, el 
siguiente: 
 
 
 
 
 
 
 
 
 
En el modelo TSO se utiliza una cola para las instrucciones ST (y SWAP, 
T&S...), es decir, escrituras, donde se asegura el orden de dichas operaciones 
(FIFO). Las instrucciones LD, en cambio, pueden adelantar dicha cola (o 
cortocircuitar resultados), siempre que no haya una dependencia de datos. En 
cambio, un ST no puede adelantar nunca un LD, ni tampoco se pueden 
adelantar los LD entre sí. De esta manera, una instrucción LD bloquea el 
acceso a memoria de las siguientes instrucciones. 
Por ejemplo, el significado de la ejecución de este programa es el 
siguiente en función del modelo de consistencia: 
 
P1 P2 
 
X = nuevo_valor; 
Y_copia = Y 
 
Y = nuevo_valor; 
X_copia = X 
 
 
SC → por lo menos una de ellas, Y_copia o X_copia, tendrá el valor nuevo. 
TSO → podría ocurrir que ni Y_copia ni X_copia tuvieran el nuevo valor. 
 
Por definición, bajo el modelo TSO/PC no se mantiene la consistencia 
secuencial y, por tanto, no se asegura que el comportamiento de los 
programas sea el “adecuado” en todos los casos. Tal vez sea necesario 
búferes ST 
(FIFO) 
LD ST 
P P P P 
 
MEM 
ST ST ST 
▪ 162 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
imponer el orden estricto (SC) en algunos puntos del programa, para lo que 
habrá que utilizar las instrucciones especiales que hemos comentado (fence). 
Si el procesador no dispone de instrucciones de ese tipo, entonces pueden 
utilizarse instrucciones read-modify-write (por ejemplo, T&S) en lugar de los 
ST (LD) habituales, ya que esas instrucciones implican una lectura y una 
escritura, y por tanto no pueden desordenarse si el modelo de consistencia es 
TSO/PC: 
 
ST ... LD → SWAP ... LD 
se pueden desordenar no se pueden desordenar 
 
El modelo TSO es adecuado para aprovechar la latencia de las escrituras, 
y bajo el mismo funciona bien la habitual sincronización mediante un flag: 
write A; write FLAG // read FLAG; read A. Ha sido utilizado en 
numerosas máquinas: Sequent Balance, Encore Multimax, (IBM 370), 
SparcCenter2000, SGI Challenge, Pentium Pro (PC), etc. 
5.3.2 Partial Store Ordering (PSO) 
En este modelo de consistencia, menos restrictivo que el anterior, se 
elimina también la restricción de orden entre escrituras; es decir, no se 
aseguran las relaciones de orden wr >> rd, wr. La implementación es 
similar a la del modelo anterior, pero las colas para las instrucciones ST no 
se gestionan en modo FIFO, con lo que no se asegura el orden de las 
escrituras. 
Hay que tener cuidado, ya que al aplicarse este modelo puede no 
funcionar correctamente la típica sincronización productor/consumidor 
mediante un flag. Por tanto, hay que analizar con cuidado si merece la pena 
su aplicación, evaluando, como siempre, lo que esperamos ganar y lo que 
podríamos perder. Como en el caso anterior, en algunos momentos puede 
que sea necesario imponer orden a las operaciones de memoria (en este caso 
para mantener también el orden wr >> wr), para lo que se utilizan las 
instrucciones especiales de ordenación (fence). 
Este modelo de consistencia se ha utilizado, por ejemplo, en el Sun Sparc 
PSO. 
5.3 MODELOS RELAJADOS (relaxed) ▪ 163 ▪ 
 
5.3.3 Modelos más relajados 
El problema del orden (consistencia) de las instrucciones de memoria sólo 
aparece en los accesos a variables compartidas, y no en el resto. Más aún; en 
los siguientes dos casos, por ejemplo, no sería necesario asegurar el orden en 
todos los accesos a memoria: 
 
P1 P2 P1 / P2 / ... / Pn 
 
X = X + 1; 
Y = B + 1; 
flag = 1; 
... 
 
... 
while (flag == 0) {}; 
A = X / 2; 
B = Y; 
 ... 
lock(cer); 
yo = i; 
i = i + N; 
j = j - 1; 
unlock(cer); 
... 
 
En realidad, bastaría con asegurar el orden en relación a las operaciones 
de sincronización; asegurado eso, da igual en qué orden se ejecuten el resto 
de las operaciones de memoria. (p.e., sólo habrá un procesador en la sección 
crítica). 
Decimos que se utiliza programación sincronizada si el uso de las 
variables compartidas se “protege” mediante operacionesde sincronización, 
tal como aparece en los ejemplos anteriores. En caso contrario, es posible 
que aparezcan “carreras de datos” (data-races), haciendo que los resultados 
obtenidos dependan, por ejemplo, de la velocidad del procesador. Por eso, la 
mayoría de los programas paralelos utilizan alguna función de 
sincronización para “ordenar” el acceso a las variables compartidas: 
funciones lock y unlock, flags, etc. 
De ser así, para mantener la consistencia (en su sentido más intuitivo), 
bastaría con asegurar el orden de las operaciones de memoria con relación 
a las de sincronización, junto con el de las de sincronización entre sí. 
Nos interesa, por tanto, distinguir los accesos “estándar” a memoria (rd, 
wr) y los accesos a variables de sincronización (s). Así, junto con las 
relaciones de orden entre operaciones rd y wr, tendremos que mantener 
también estas otras: 
 
 rd, wr >> s s >> rd, wr s >> s 
 
Para aplicar un tratamiento específico a las operaciones de sincronización, 
habrá que identificarlas convenientemente (por hardware y/o por software). 
▪ 164 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
5.3.3.1 Weak Ordering (WO) 
En el modelo de consistencia Weak Ordering se admite cualquier orden en 
las operaciones de memoria que no sean de sincronización, mientras que se 
impone orden estricto a estas últimas (que se van a comportar como si fueran 
instrucciones de tipo fence). En resumen, antes de ejecutar una operación de 
sincronización hay que esperar a la finalización (global) de todas las 
operaciones de memoria anteriores; de igual manera, las operaciones de 
memoria posteriores deberán esperar a que finalice por completo la 
operación de sincronización. 
Estas son pues las relaciones de orden a mantener: 
 
 rd / wr >> s; s >> rd / wr; s >> s. 
 
 
 
 
Como en los dos casos anteriores, si se necesita imponer el orden estricto 
en una determinada zona del programa, una de dos: o se usan instrucciones 
fence o, si no existe esa posibilidad, se identifican como operaciones de 
sincronización las instrucciones LD o ST correspondientes. 
5.3.3.2 Release Consistency (RC) 
Se trata del modelo de consistencia más flexible. Como en el caso 
anterior, son las operaciones de sincronización las que van a marcar los 
puntos de ordenación del programa; entre ellas, los LD y ST pueden 
ejecutarse en cualquier orden. Pero además, las operaciones de 
sincronización se dividen en dos tipos: adquisición (acquire, sa) y 
liberación (release, sr). Las operaciones sa son lecturas (u operaciones 
RMW), y las operaciones sr escrituras (u operaciones RMW). Por ejemplo, 
una función de lock es una operación de sincronización de tipo acquire, 
mientras que unlock es de tipo release. 
Junto con el orden entre operaciones de sincronización (s >> s), se deben 
mantener estos otros: 
• las operaciones de memoria posteriores a operaciones de 
sincronización tipo adquisición (acquire) deben esperar a que 
terminen éstas; es decir, hay que mantener el orden sa >> rd / wr. 
 
rd ...wr ... 
 
sinc 
 
rd ...wr ... 
 
sinc 
 
 
 
rd ...wr ... 
5.3 MODELOS RELAJADOS (relaxed) ▪ 165 ▪ 
 
• antes de ejecutar una operación de sincronización tipo liberación 
(release), el procesador debe esperar a que finalicen todas la 
operaciones de memoria anteriores; es decir, hay que mantener el 
orden rd / wr >> sr. 
 
 
 
 
 
Estos dos últimos modelos de consistencia son adecuados en los casos de 
planificación dinámica de las instrucciones (desorden/desorden), puesto que 
se acepta la finalización en desorden de las instrucciones LD y el 
adelantamiento de los ST. Los procesadores Alpha, IBM PowerPC, MIPS 
utilizan un modelo de consistencia de este tipo (en muchos casos no se aplica 
ningún modelo concreto, y se deja al usuario que defina el modelo que desea 
mediante el uso de instrucciones fence). 
 
 
En la siguiente tabla se resumen las características principales de los 
modelos de consistencia. 
 
Modelo 
Orden de las operaciones de memoria Instrucc. para 
imponer orden wr>>rd wr>>wr rd>>rd/wr sinc. wr atom. 
SC    todas  
TSO   todas  MEMBAR, RMW 
PC   todas MEMBAR, RMW 
PSO  todas  STBAR, RMW 
WO todas  SYNC 
RC 
sa >> w/r 
w/r >> sr 
s >> s 
 REL, ACQ, RMW 
 
rd ...wr ... 
 
s_acq 
 
rd ...wr ... 
 
s_rel 
 
 
rd ...wr ... 
▪ 166 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
En el ejemplo de la siguiente figura aparecen remarcadas las restricciones 
de orden que impone cada modelo. 
 
 SC 
wr,rd,s >> wr,rd,s 
TSO/PC 
– wr >> rd 
PSO 
– wr >> wr 
 WO RC 
– rd >> wr, rd 
 
rd 
wr 
sinc_a 
wr 
rd 
sinc_r 
wr 
wr 
 
 = A 
B = 
sinc_acq 
C = 
 = D 
sinc_rel 
E = 
F = 
 
 = A 
B = 
sinc_acq 
C = 
 = D 
sinc_rel 
E = 
F = 
 
 = A 
B = 
sinc_acq 
C = 
 = D 
sinc_rel 
E = 
F = 
 
 = A 
B = 
sinc_acq 
C = 
 = D 
sinc_rel 
E = 
F = 
 
 = A 
B = 
sinc_acq 
C = 
 = D 
sinc_rel 
E = 
F = 
 
 
 
5.4 RESUMEN Y PERSPECTIVAS 
Para que los programas paralelos tengan una semántica clara, tanto el 
hardware como el programador necesitan que el multiprocesador tenga una 
“imagen de memoria” bien definida. A la imagen o interfaz de memoria del 
multiprocesador se le denomina modelo de consistencia. 
Existen dos tipos de modelos de consistencia: el secuencial y los 
relajados. El primero, SC, impone el orden local y global de todas las 
operaciones de memoria, así como la atomicidad de las mismas. Los 
modelos relajados, en cambio, permiten el desorden de algunas de esas 
operaciones; por ejemplo, pueden adelantarse los LD (TSO), o los LD y los 
ST (PSO), o puede admitirse cualquier orden entre ellas pero respetando el 
orden con relación a las operaciones de sincronización (WO). Cuando se 
utilizan modelos de consistencia relajados, en algunos casos es necesario 
imponer el orden estricto, para lo que se utilizan instrucciones especiales 
denominadas fence. 
5.4 RESUMEN Y PERSPECTIVAS ▪ 167 ▪ 
 
Si consideramos el rendimiento del sistema, los modelos relajados 
debieran ser más eficientes que el modelo estricto SC, ya que en este caso no 
pueden aplicarse muchas de las optimaciones más habituales, y, en 
consecuencia, la eficiencia debiera ser menor. Pero como siempre, debemos 
analizar los aspectos positivos y negativos de la aplicación de modelos de 
consistencia relajados, ya que para poder aplicar estos modelos se necesita la 
colaboración del hardware y del software (nuevas instrucciones, identificar 
correctamente los puntos de ordenación dentro del programa, etc.). 
Uno de los investigadores principales de estas cuestiones es Mark Hill. En 
su opinión, los multiprocesadores deberían utilizar SC como modelo básico, 
y, tal vez, ofrecer como alternativa un modelo relajado. ¿Por qué? 
En los procesadores actuales es habitual el uso de ejecución especulativa 
de las instrucciones. Las instrucciones se ejecutan sin estar seguro de que 
hay que hacerlo. Cuando su ejecución se convierte en segura, se escriben los 
resultados, las instrucciones se dan por finalizadas y se retiran del 
procesador (commit); en caso contrario, si se comprueba que no había que 
haberlas ejecutado, entonces hay que deshacer el efecto de esas instrucciones 
(en muchos casos, ejecutando procedimientos de roll-back), y volver a un 
punto “seguro” en la ejecución del programa. 
Siendo eso así, aunque el modelo de consistencia sea SC podrían aplicarse 
las optimizaciones habituales, en la medida en que se haga de manera 
especulativa; si no resultan adecuadas, tendremos la posibilidad de 
deshacerlas. ¿En qué se diferenciarían entonces ambos tipos de consistencia? 
Pues en que, en el caso de los modelos relajados, las instrucciones se 
retirarían antes del procesador, ya que no habría que esperar a saber si la 
reordenación efectuada ha sido correcta o no. 
Siempre es necesario medir las hipotéticas ventajas de cualquier tipo de 
estrategia utilizando programasreales o bancos de pruebas. Algunos 
experimentos realizados muestran que los tiempos de ejecución pueden 
llegar a ser un 10% - 20% menores en el caso de los modelos relajados que 
en el modelo SC. ¿Merece la pena esa mejora? ¿Aceptan los diseñadores de 
middleware (software del sistema, aplicaciones en bajo nivel...) la 
complejidad inherente al uso de modelos de consistencia relajados? Por 
ejemplo, corresponde a los diseñadores de compiladores introducir las 
instrucciones fence (las estrictamente necesarias y no más, para no perder 
eficiencia); para facilitar la portabilidad del software hay que implementar 
adecuadamente todos los modelos para poder trabajar en plataformas 
▪ 168 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS 
hardware diferentes; etc. Programar en paralelo es difícil en sí mismo, y más 
aún si hay que considerar modelos relajados de consistencia. 
En resumen: el modelo SC es el estándar en todos los multiprocesadores, 
los problemas de consistencia se resuelven en hardware y son transparentes 
para el programador. Como segunda alternativa, el modelo TSO parece el 
adecuado para poder aplicar las optimizaciones más habituales (adelantar los 
LD) y sus efectos sobre el programador son bajos. Los modelos que eliminan 
cualquier restricción en el orden de las operaciones de memoria parecen más 
difíciles de justificar. 
▪ 6 ▪ 
 
 
La Red de Comunicación de 
los Computadores Paralelos. 
Comunicación mediante 
Paso de Mensajes. 
 
 
 
 
 
 
 
6.1 INTRODUCCIÓN 
Los principales componentes de un sistema paralelo son los computadores 
(procesador, memoria...), la red de comunicación y el interfaz entre ambos. 
En los capítulos anteriores hemos analizado los multiprocesadores que 
utilizan un bus como red de interconexión (sistemas SMP). En estos sistemas 
el espacio de direccionamiento es común para todos los procesadores y el 
tiempo de acceso a cualquier posición de memoria es el mismo desde 
cualquier procesador. Pero el bus no es una red adecuado para interconectar 
los procesadores de un sistema paralelo cuando el número de nodos es 
▪ 170 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
elevado, ya que la cantidad de tráfico que puede soportar un bus es limitada. 
Dicho tráfico, sin embargo, crece según aumenta el número de procesadores 
del sistema, y, además, no de manera lineal, por lo que los sistemas SMP 
más habituales suelen ser de 4 – 8 procesadores (o, como mucho, hasta 32). 
Para construir sistemas con centenares o miles de procesadores tenemos 
que utilizar otras estructuras y otro tipo de redes de comunicación. Sea 
privada o sea compartida, es necesario distribuir la memoria entre los 
diferentes nodos del sistema, con lo que ya no podremos usar un bus como 
red de conexión. En este capítulo vamos a analizar las redes de interconexión 
que se utilizan en los sistemas paralelos, tanto de memoria compartida 
(DSM) como de memoria privada (MPP). 
La necesidad de comunicación entre procesadores no surge con los 
sistemas MPP. Las redes de ordenadores son "antiguas" en el mundo de la 
informática; por ejemplo, la red ARPANET es de 1969. Sin embargo, a 
pesar de compartir ciertos aspectos con las redes denominadas LAN y WAN 
(local & wide area network), las redes de interconexión para sistemas MPP 
son especiales, ya que la necesidad de comunicación es mucho mayor, y, 
además, se debe llevar a cabo en mucho menos tiempo. Aunque en los dos 
casos se pasa información entre los procesadores, no se pueden comparar las 
necesidades de comunicación asociadas a la resolución de un sistema de 
ecuaciones mediante un sistema paralelo y el envío de un mensaje de correo 
electrónico entre dos usuarios de Internet. En el primer caso, la 
comunicación se deberá completar en microsegundos, siendo la latencia un 
aspecto crítico para lograr una ejecución adecuada del problema; en el 
segundo caso, en cambio, no. Otro aspecto que diferencia ambos ámbitos es 
la distancia entre los procesadores, que en las redes LAN y WAN suele ser 
mucho mayor (puede variar desde varios metros a kilómetros) que en los 
sistemas MPP (como máximo, de unos pocos metros). 
En cualquier caso, no podemos olvidar arquitecturas intermedias de gran 
difusión como los clusters o similares, especialmente interesantes desde el 
punto de vista del binomio coste/rendimiento. En esos sistemas se están 
utilizando tanto redes de banda ancha provenientes del mundo de las redes 
de computadores (por ejemplo, Gigabit Ethernet), como redes de diseño 
específico, más rápidas y más caras (por ejemplo, InfiniBand o Myrinet). 
La función de una red de comunicación en un sistema MPP es clara: en 
función del algoritmo que se está ejecutando en el sistema paralelo, llevar la 
información de un procesador a otro. Además, la latencia de la 
comunicación debe de ser lo más baja posible, se deben admitir muchas 
6.2 TOPOLOGÍA DE LA RED ▪ 171 ▪ 
comunicaciones simultáneamente, el coste de la red debe ser bajo, y, en 
cierta medida, el sistema debe mantenerse operativo a pesar de que pueda 
haber algunos fallos en la red. Y si es posible, todo lo anterior debería ser 
independiente del número de procesadores que estén conectados al sistema. 
La red de comunicación de los sistemas paralelos que hemos analizado 
hasta el momento ha sido muy simple: un bus. Desde el punto de vista del 
coste, un bus es una red adecuada (barata), pero tiene muchos 
inconvenientes. Cuando se utiliza un bus como mecanismo de interconexión 
los procesadores deben compartir en el tiempo el ancho de banda del bus (no 
se pueden enviar dos mensajes a la vez); por este motivo, no se pueden 
conectar demasiados procesadores mediante un bus, ya que cada vez se 
producirían más problemas en la comunicación (colisiones), y, en 
consecuencia, las latencias de las comunicaciones crecerían excesivamente. 
Si nos vamos al otro extremo, y conectamos todos los procesadores con 
todos, obtenemos un crossbar. Desde el punto de vista del rendimiento de 
las comunicaciones es la mejor red posible, ya que cada procesador tiene un 
enlace privado con todos y cada uno de los procesadores del sistema; los 
componentes de la red no se deben compartir, por lo que la latencia de las 
comunicaciones va a ser mínima. Evidentemente, el coste de esta red es muy 
alto, y, además, dicho coste crece exponencialmente con el número de 
procesadores a interconectar. Por tanto, debemos encontrar otras formas de 
conectar un número elevado (miles) de procesadores. 
En una red de comunicación podemos distinguir dos aspectos. Por un 
lado, el hardware: los conmutadores o encaminadores de mensajes, los 
enlaces o links, y el interfaz (conexión del procesador con la red). Por otro 
lado, el “software”: protocolos de comunicación, a diferentes niveles. Para 
definir una red, se deben concretar diferentes aspectos: la topología, el 
algoritmo de encaminamiento, la estrategia de conmutación, el control de 
flujo... En los próximos apartados vamos a analizar estos aspectos, 
comenzando por la topología de una red, y terminando con los conflictos de 
comunicación en este tipo de redes. 
 
6.2 TOPOLOGÍA DE LA RED 
La topología de la red de comunicación determina las conexiones 
existentes entre los procesadores, y podemos analizarla desde el punto de 
vista de la teoría de grafos. Los nodos del grafo representan a los 
procesadores (o, más precisamente, a los gestores de la comunicación), y los 
▪ 172 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
arcos del grafo se corresponden con los enlaces de la red. El comportamiento 
de la red se puede estudiar utilizando diferentes parámetros topológicos. Por 
tanto, antes de analizar las redes más utilizadas, definamos algunos de estos 
parámetros. 
 
• Distancia media, d: es la media de las distancias entre todos los 
posibles pares de nodos. Para calcular la distancia entre dos nodos se 
contabiliza el número de nodos que hay que atravesar para ir desdeel 
nodo origen al nodo destino utilizando el camino de longitud mínima. 
Por tanto: 
)1(
1,
,
−
=
∑
=
PP
d
d
P
ji
ji
 
• Diámetro, D: es la distancia máxima que existe entre cualquier par de 
nodos del grafo (tomando la distancia mínima de cada par de nodos). 
El diámetro está relacionado con la latencia máxima que podría tener 
una comunicación, en ausencia de otros problemas (tráfico, etc.). 
 Ambos parámetros, distancia media y diámetro, nos dan una primera 
aproximación a la latencia de las comunicaciones en la red (en 
promedio y en el caso peor) cuando la comunicación es aleatoria. 
 
• Grado: indica el número de enlaces que tiene un nodo (procesador). 
Si el grado de todos los nodos es el mismo, se dice que la red es 
regular. Interesa que las redes estén compuestas por nodos con grados 
relativamente bajos (grado 4, por ejemplo), ya que si el grado es alto 
el número de conexiones también lo será, con lo que la 
implementación de la red puede llegar a ser muy compleja. 
 
• Simetría: cuando todos los nodos tienen la misma visión de la red se 
dice que la red es simétrica. Es una característica deseable, ya que 
facilita la elección de los caminos para la comunicación. 
 
• Escalabilidad: una red de comunicación debería ser fácilmente 
ampliable, para permitir aumentar, sin grandes problemas, el número 
de procesadores. 
 
• Tolerancia a fallos: una red de comunicación debe ser segura; el 
sistema debe seguir funcionando aunque algún componente de la red 
deje de funcionar. Se trata de una condición imprescindible si el 
número de nodos es elevado, porque la probabilidad de que algún 
6.3 REDES FORMADAS POR CONMUTADORES ▪ 173 ▪ 
nodo falle crece con el número de nodos del computador. En la misma 
línea, es una condición básica para aquellos sistemas que deben estar 
siempre en funcionamiento y cuyo mantenimiento es muy difícil o 
imposible. 
 
• Conectividad: la conectividad de una red determina el número 
mínimo de enlaces o nodos —arco-conectividad o nodo-
conectividad— que se deben estropear para que la red quede dividida 
en dos o más trozos. Es un parámetro relacionado con la tolerancia a 
fallos. 
 
• Bisección de la red: es el número mínimo de enlaces que se deben 
cortar (eliminar) para dividir la red en dos partes iguales. Como 
veremos más adelante, este parámetro da una idea del tráfico máximo 
(throughput) que puede gestionar una red (bajo unas determinadas 
condiciones). 
 
• Tipo de enlace: los enlaces entre nodos pueden ser unidireccionales 
—es decir, A→B—, o bidireccionales —esto es, A↔B—. El tipo de 
enlace tiene gran influencia en los parámetros de una red relacionados 
con la distancia (es decir, con la comunicación). 
 En función del tipo de enlaces, las redes se suelen dividir a veces en 
dos categorías: directed, si los enlaces tienen una dirección marcada; y 
non-directed (o undirected), si los enlaces aceptan comunicación en 
ambos sentidos. 
 En general, vamos a trabajar con enlaces bidireccionales. 
 
Después de haber presentado algunos parámetros topológicos, analicemos 
las principales redes de comunicación, tanto estáticas como dinámicas. A 
pesar de que en la literatura existe una gran cantidad de propuestas, sólo 
vamos a presentar las más utilizadas. 
 
6.3 REDES FORMADAS POR CONMUTADORES 
La función de la red de comunicación de un sistema paralelo es permitir la 
comunicación entre procesadores (o entre procesadores y memoria), es decir, 
transmitir datos. Las redes de transmisión de datos se han utilizado desde 
hace mucho tiempo en otras áreas tecnológicas, más en concreto en la 
telefonía. Por ello, se ha aprovechado la experiencia acumulada durante años 
▪ 174 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
en esa área y se ha adaptado el uso de las redes más habituales en telefonía, 
redes construidas a partir de conmutadores, a los sistemas de cómputo 
paralelo. A estas redes se les conoce también como redes dinámicas, porque 
el camino de comunicación entre origen y destino se construye 
dinámicamente (no existen enlaces fijos entre los nodos). Analicemos en qué 
consisten y cómo se usan estas redes. 
6.3.1 El conmutador (switch) 
Comencemos describiendo qué es un conmutador, “dispositivo” que sirve 
para construir redes dinámicas. El conmutador más simple, de grado k = 2, 
es un dispositivo que conecta dos entradas (E0 y E1) y dos salidas (S0 y S1). 
Mediante él, y en función de una señal de control, se pueden establecer las 
siguientes cuatro conexiones27: 
 
 
 
 
 
 
 
 
 
Por medio de las dos primeras conexiones, la información de una de las 
entradas se distribuye a ambas salidas (ese tipo de comunicación se conoce 
como broadcast). Sin embargo, las conexiones que más nos interesan son las 
otras dos, en las que se conecta cada entrada con una salida, seguidas o 
cruzadas, para transmitir información. 
Visto desde otro punto de vista, mediante un conmutador se consigue una 
“permutación” de las entradas en las salidas. Por ejemplo, con un 
conmutador de grado 2 podemos establecer las dos siguientes conexiones: 
(0, 1) → (0, 1) o (0, 1) → (1, 0), es decir, las dos permutaciones de las 
entradas. 
 
27 En su versión más simple, un conmutador está formado por unos multiplexores y la lógica necesaria 
para su control. 
E0 → S0, S1 E1 → S0, S1 
E0 → S0 
E1 → S1 
E0 → S1 
E1 → S0 
0 
1 
0 
1 
Conmutador 
k = 2 
 
E0 
E1 
S0 
S1 
Señales de control 
6.3 REDES FORMADAS POR CONMUTADORES ▪ 175 ▪ 
Las conexiones que se crean en cada conmutador son "dinámicas", ya que 
van a ir cambiado en el tiempo en función de las necesidades de 
comunicación. 
6.3.2 Red crossbar 
Tal y como hemos comentado anteriormente, la red de comunicación ideal 
sería la que permitiera, en cualquier momento y en un solo "paso", que se 
comunicaran cualquier par de procesadores simultáneamente. Según lo 
hemos definido, un conmutador de P entradas cumple con esa condición: 
permite efectuar P conexiones simultáneas. A esta red se la denomina 
también crossbar. 
Un crossbar puede construirse de muchas maneras, generalmente 
mediante conmutadores de menor grado. En la figura se muestra un ejemplo 
de implementación de un crossbar que conecta cuatro procesadores entre sí, 
o con cuatro módulos de memoria, construido mediante conmutadores de 
grado 2. Cada conmutador permite conectar una fila con una columna, y 
ofrece la posibilidad de seguir o girar. 
 
 
 
 
 
 
 
Tal como aparece en la figura, esta estructura de conmutadores permite 
efectuar simultáneamente P comunicaciones (cualesquiera, siempre que los 
destinos sean todos diferentes). Sin embargo, el coste de esta red es muy alto 
si el número de procesadores es elevado, ya que el número de conmutadores 
y de conexiones (la complejidad de la red) es del orden de P2; además, el 
control tiene que ser centralizado. 
Las redes de tipo crossbar se han venido utilizando, normalmente, con un 
número pequeño de procesadores, aunque en ocasiones también en sistemas 
con un número de procesadores elevado (por ejemplo, en el computador 
Earth Simulator), a veces con conmutadores organizados en varias etapas. 
Conmutadores 
Procesadores o 
módulos de memoria 
0 
1 
2 
3 
0 
1 
2 
3 
▪ 176 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
6.3.3 Redes multietapa (multistage) 
El coste de los conmutadores crece cuadráticamente con el número de 
entradas. Por tanto, para reducir el coste de la red (para usar menos 
hardware) es necesario organizar la red de otra manera, aunque con ello no 
obtengamos la misma capacidad de comunicación y latencia reducida que 
podemos conseguir con un crossbar. 
Las redes dinámicas más utilizadas son las denominadas redes multietapa, 
en las que las conexiones se establecen mediante conmutadores organizados 
por niveles o etapas, tal y como se muestra en la siguiente figura. Las 
conexiones entre etapa y etapa se pueden definir de múltiplesmaneras; en 
función del esquema o patrón de conexionado se obtienen diferentes tipos de 
redes: perfect shuffle, butterfly, cube connection, etc. 
 
 
 
 
 
 
 
 
 
 
 
 
La red del dibujo es unidireccional (de izquierda a derecha); si se necesita que 
sea bidireccional (p.e., para el caso de conexiones con módulos de memoria) 
basta con superponer dos redes unidireccionales. 
 
6.3.3.1 La red Omega 
Como ejemplo de las redes multietapa veamos una de las más utilizadas: 
la red Omega. Una red Omega con P entradas consta de logk P niveles o 
etapas de conmutación, cada una con P/k conmutadores. Por tanto, el 
número de conmutadores de una red Omega de grado k es (P/k) logk P, 
mucho menor que el de un crossbar. Por ejemplo, un crossbar de 256 nodos 
utiliza 2562 = 65.536 conmutadores de grado 2, mientras que la red Omega 
correspondiente sólo utiliza 128 × 8 = 1.024; por contra, la latencia de las 
Procesadores Procesadores 
(o módulos de memoria) 
Conexiones entre etapas 
de conmutación 
Conmutadores 
 
0 
P–1 
0 
P–1 
 
6.3 REDES FORMADAS POR CONMUTADORES ▪ 177 ▪ 
conexiones es mayor en la red Omega, ya que hay que superar 8 etapas de 
conmutación (una sola en el crossbar). 
Las conexiones entre las etapas de conmutación siguen un esquema 
denominado barajado perfecto (perfect shuffle), tal y como se muestra en la 
figura (con conmutadores de grado 2). 
 
 
 
 
 
 
 
 
 
 
 
El esquema de conexionado denominado barajado perfecto es muy 
simple: para el caso de grado 2, las P entradas (de 0 a P–1) se dividen en dos 
grupos por la mitad, y se reordenan de la siguiente manera: una de la primera 
mitad (0), otra de la segunda mitad (P/2); una de la primera mitad (1), otra 
de la segunda mitad (P/2+1); etc. Es decir, se efectúa la siguiente 
permutación: 
 
[0, 1, 2... P–1] → [0, P/2, 1, P/2+1, ... P/2–1, P–1]. 
 
En el caso general de grado k, se dividen en k grupos y se reordenan de la 
misma manera que acabamos de comentar. Se puede comprobar que la 
permutación de barajado perfecto se corresponde con una rotación hacia la 
izquierda de la dirección binaria del origen. La tabla siguiente muestra dicha 
rotación para el caso de 8 procesadores. 
 
Barajado perfecto (perfect shuffle) 
 
posición antigua nueva posición 
(0) 000 → (0) 000 
(1) 001 → (2) 010 
(2) 010 → (4) 100 
(3) 011 → (6) 110 
(4) 100 → (1) 001 
(5) 101 → (3) 011 
(6) 110 → (5) 101 
(7) 111 → (7) 111 
0 
2 
4 
6 
1 
3 
5 
7 
0 
2 
4 
6 
1 
3 
5 
7 
0 
0 
1 
2 
3 
5 
6 
7 
4 
0 
1 
Barajado perfecto 
Red Omega 
de 8 nodos 
or
ig
en
 
de
st
in
o 
▪ 178 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
Analicemos los parámetros topológicos de una red Omega. La distancia 
media y el diámetro coinciden: todos los procesadores están a la misma 
distancia, logk P, el número de etapas de la red. Por tanto, desde el punto de 
vista topológico la latencia de todos los mensajes sería la misma: no se 
puede aprovechar la “localidad” de las comunicaciones (la comunicación 
suele ser más frecuente con los procesadores que están más "cerca"). 
La red es simétrica y regular. El grado de los conmutadores es fijo y el 
mismo para todos (puede ser 2, como en la figura, pero son más habituales 
los conmutadores de grado 4). La red no tiene tolerancia a fallos; tal como 
vamos a ver a continuación, sólo existe un camino que comunique el nodo i 
con el nodo j, por lo que si algo falla en dicho camino no es posible 
establecer esa comunicación. En algunos casos, si no se rompe la red, una 
comunicación entre i y j que no se puede establecer puede dividirse en dos 
fases: se manda un mensaje de i a k, y se reenvía luego de k a j (aunque, 
obviamente, la latencia total será mucho mayor). Si no, la única opción para 
hacer frente a fallos en la red consiste en replicar el hardware (p.e. duplicar 
los conmutadores, para que si uno de ellos falla podamos usar el otro). 
6.3.3.2 Encaminamiento en la red Omega 
La finalidad de una red de comunicación es, evidentemente, permitir la 
comunicación entre los procesadores. Por ello, es importante que se pueda 
conectar el origen y el destino de una manera fácil. Pero, ¿cómo se construye 
una conexión (un camino) entre dos nodos de la red? A la construcción (o 
elección) de un camino determinado se le denomina encaminamiento 
(routing). En las redes Omega el encaminamiento es bastante simple. 
Si analizamos el funcionamiento de los conmutadores (k = 2) y de la red 
Omega, se puede ver que cuando se elige una salida en cada conmutador se 
modifica el último bit de la dirección (posición): a 0, si se elige la salida de 
arriba, o a 1, si se elige la de abajo. Además, en cada uno de los barajados 
que se hace entre las etapas de conmutación se rota un bit de la dirección. 
Por ello, para llegar al destino basta tener en cuenta la dirección de destino: 
la salida escogida en cada nivel de conmutación se va correspondiendo con 
los bits de la dirección de destino, comenzando por el bit de más peso. 
Veamos un ejemplo (el de la figura anterior). Para ir desde el procesador 4 
(100) al 2 (010), hay que elegir el siguiente camino en los conmutadores: 
arriba (0), abajo (1) y arriba (0). 
 
6.3 REDES FORMADAS POR CONMUTADORES ▪ 179 ▪ 
 barajado barajado barajado 
100 → 001  000 → 000  001 → 010  010 
 salida superior salida inferior salida superior 
 
Otra alternativa para seleccionar el camino es calcular la función xor 
entre las direcciones del origen y del destino. El resultado, denominado 
routing record o registro de encaminamiento (RE), se debe utilizar de la 
siguiente manera: en cada etapa, se procesa un bit del registro de 
encaminamiento, comenzando por el de más peso; si es 0, se prosigue sin 
cruzar; en cambio, si es 1, se cruza dentro del conmutador, eligiendo la 
salida contraria. 
Para el ejemplo anterior, 4 → 2: RE = 100 xor 010 = 110. Por tanto, en 
las dos primeras etapas cruzar, y en la tercera no cruzar. 
Sea utilizando la dirección de destino o el registro de encaminamiento, es 
muy sencillo encontrar el camino para ir del nodo i al nodo j en una red 
Omega. Sin embargo, existe sólo un camino para ir de un nodo a otro, lo 
cual no resulta muy favorable si se quiere poder hacer frente a fallos de 
funcionamiento de la red (o a situaciones de tráfico elevado). 
6.3.3.3 Conflictos de salida y bloqueos 
Uno de los objetivos de una red de interconexión es poder realizar más de 
una comunicación a la vez, lo cual no es posible en el caso de un bus. Si 
tenemos P procesadores en la red, en el mejor de los casos podríamos tener 
P comunicaciones simultáneamente (por supuesto, siendo diferentes los 
destinos); tal y como hemos comentado, una crossbar permite efectuar 
siempre esas P comunicaciones. 
Una red Omega también permite efectuar P comunicaciones 
simultáneamente, pero, a pesar de tener toda la conectividad entre entradas y 
salidas —ya que no se ponen restricciones al encaminamiento—, no es 
posible efectuar esa comunicación en todos los casos, ya que en muchos de 
ellos se generan conflictos en el uso de los recursos de la red. 
Por ejemplo, en una red Omega no es posible realizar estas dos 
comunicaciones a la vez: 0 → 1 y 6 → 0. 
 
 
 
▪ 180 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
 
 
 
 
 
 
 
 
Como se puede observar, los dos mensajes quieren tomar la misma salida 
en el segundo conmutador. Por tanto, surge un conflicto en dicha salida, y se 
debe de tomar una decisión. La decisión puede ser una de estas dos: o se 
rechaza una de las comunicaciones (y en muchos casos eso no se puede 
permitir, ¿se enterará el emisor? ¿cuándo?), o se guarda uno de los 
“mensajes” en un búfer hasta que quede libre la salida. En este último caso, 
se complica el diseño del conmutador —hay que añadir búferes, autómatas 
para gestionarlos...— y, en consecuencia, el tiempo de respuesta del 
conmutador será más alto. 
Si esos conflictos se producen a menudo, el rendimiento de la redde 
comunicación no será muy bueno. Podemos calcular de manera sencilla 
cuántas permutaciones (P comunicaciones simultáneas) se pueden realizar 
sin conflicto en una red Omega en comparación con el número total de 
permutaciones posibles. Para P procesadores, el número total de 
permutaciones es P! (por ejemplo, para el caso de P = 3, son 6: 012 → 012, 
021, 102, 120, 201, 210). En una red Omega (k = 2), cada conmutador ofrece 
dos posibilidades (seguir o cruzar), y tenemos (P/2) × log2 P conmutadores; 
por tanto, el número de permutaciones que se pueden realizar es: 
 
2
log
2 22
PPP
P= [ en general, si el grado es k: 
P
k
P
k
k
log
)!( ] 
 
A medida que crece la red, el porcentaje de comunicaciones sin conflicto 
se va reduciendo (P = 8 → 10%; P = 16 → 0,02%; en todo caso, se trata de 
un número muy grande). Sin embargo, en muchas aplicaciones algunas de 
esas permutaciones son mucho más comunes que otras (por ejemplo, la 
permutación Pi → Pi+1 es muy habitual en muchas aplicaciones). Por eso, a 
pesar de que no se puedan realizar todas las permutaciones, una red de 
comunicación adecuada debe de poder realizar las más utilizadas. 
0 
2 
4 
6 
1 
3 
5 
7 
0 
2 
4 
6 
1 
3 
5 
7 
6.3 REDES FORMADAS POR CONMUTADORES ▪ 181 ▪ 
Si al tener que efectuar una determinada permutación (por ejemplo, en una 
máquina SIMD) supiéramos que se van a generar conflictos por los recursos, 
podríamos desdoblar la permutación en dos permutaciones que sí se 
pudieran realizar. Tal como hemos comentado, esa misma solución se puede 
aplicar para hacer frente a los fallos de funcionamiento de la red. 
En resumen: una red Omega es una red "bloqueante" (blocking network), 
ya que no admite, simultáneamente, implementar cualquier comunicación, 
pero, a pesar de ello, es una red adecuada y ampliamente utilizada, porque en 
muchos casos (los más utilizados) no presenta problemas para que todos los 
nodos puedan comunicarse entre sí. 
6.3.3.4 Otro patrón de comunicación: broadcast 
Las permutaciones son un tipo de comunicación muy utilizado en 
determinadas aplicaciones, pero no el único. Por ejemplo, es habitual tener 
que efectuar un tipo de comunicación que se conoce como broadcast: enviar 
información desde un procesador a todos los demás i → j, ∀j. 
Este tipo de comunicación se puede realizar de un modo sencillo en una 
red Omega. Los conmutadores, tal y como hemos visto anteriormente, tienen 
la posibilidad de enviar una entrada a las dos salidas. Por tanto, repitiendo 
esa operación en todas las etapas se produce un broadcast. Por ejemplo para 
efectuar un broadcast desde el nodo 5: 
 
 
 
 
 
 
 
 
6.3.3.5 Otras redes 
Podríamos enumerar muchos otros ejemplos de redes multietapa que se 
han usado en multiprocesadores. Entre las más conocidas está la denominada 
red butterfly (mariposa). En la figura se presenta un esquema de una red 
butterfly de 16 nodos, con conmutadores de grado 2 o de grado 4. 
0 
2 
4 
6 
1 
3 
5 
7 
0 
2 
4 
6 
1 
3 
5 
7 
BC 
BC 
BC 
BC 
BC 
BC 
BC 
▪ 182 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
 
 
 
 
 
 
 
 
 
red butterfly (k = 2) red butterfly (k = 4) 
 
Las redes butterfly y Omega son muy similares; por ejemplo, puede 
comprobarse fácilmente que puede usarse en ambas el mismo algoritmo de 
encaminamiento, el que utiliza la función xor para generar el registro de 
encaminamiento. 
Las redes Omega y butterfly, son redes bloqueantes; por su parte, un 
crossbar es una red no bloqueante (admite cualquier comunicación 
simultánea), pero su coste es muy elevado. En todo caso, es posible construir 
redes no bloqueantes con un coste menor que el del crossbar.; ese tipo de 
redes se conoce, de manera general, como redes de Clos. Entre las redes no 
bloqueantes se encuentran las redes "reorganizables" (rearrangeable), en las 
que siempre es posible encontrar un camino para cualquier comunicación, 
aunque en algunas ocasiones es necesario reorganizar los caminos existentes 
en un momento dado. Un ejemplo de ese tipo de redes son las redes de 
Benes, que permiten cualquier permutación si se conoce de antemano cuál es 
(para elegir los caminos adecuados), ya que hay muchos caminos para 
conectar origen y destino. 
La siguiente figura presenta una red de Benes de 16 nodos, que utiliza 7 
etapas de conmutación (en las redes de Clos el número de etapas de 
conmutación es siempre un número impar). La red permite construir 
múltiples caminos para unir dos nodos; por ejemplo, hemos dibujado dos 
caminos diferentes para ir del nodo P3 al P11. Como es obvio, el coste de 
esta red es mayor que el de una red Omega, y la latencia de los paquetes será 
también mayor, ya que tienen que superar más etapas de comunicación. 
Si "doblamos" una red de Benes sobre sí misma, la red que se consigue se 
denomina árbol (fat tree), en la que los paquetes van hacia adelante (hasta la 
raíz) y luego vuelven hacia atrás para llegar al destino (es decir, los enlaces y 
puertos son bidireccionales). 
6.3 REDES FORMADAS POR CONMUTADORES ▪ 183 ▪ 
 
 
 
 
 
 
 
 
 
 
Red de Benes Árbol (fat tree) 
 
6.3.3.6 Resumen 
Las redes multietapa intentan superar algunas de las limitaciones que nos 
encontramos cuando utilizamos un bus como red de comunicación. Este tipo 
de redes tienen larga historia en el área de paralelismo. Al principio se 
utilizaron en los sistemas SIMD, en los que la unidad de control distribuye 
las instrucciones de un único programa entre los procesadores para 
ejecutarlas en modo síncrono. Para algunas aplicaciones —por ejemplo, 
tratamiento de imagen, cálculo matricial, aplicaciones numéricas, etc.— este 
tipo de máquinas es adecuado, dado que el reparto de trabajo y la 
transferencia de datos es muy regular: se aprovecha el paralelismo de datos. 
En relación con el reparto de datos, es muy común que los esquemas de 
comunicación sean permutaciones (por ejemplo, i → i+1), o tener que hacer 
un broadcast... Además de en máquinas tipo SIMD, también se han utilizado 
este tipo de redes en multicomputadores MIMD, para sustituir al bus; sin 
embargo, hoy en día no se utilizan demasiado (salvo en forma de fat tree y 
topologías similares, tal como veremos a continuación). 
En la siguiente tabla se presenta una comparación entre un bus, una red 
crossbar y una red Omega. Si no hay conflictos, la latencia de las 
comunicaciones en un bus es constante; en una red Omega, en cambio, la 
latencia es mayor y función de P. En contrapartida, pueden efectuarse más 
comunicaciones simultáneamente, ya que el bus hay que compartirlo entre 
todos los procesadores (w/P), y la red Omega no (el "ancho de banda" es 
mayor). En todo caso, la red Omega es más compleja que un simple bus. 
 
 
▪ 184 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
P procesadores / canales de w bits 
conmutadores de grado k 
(O(x) = orden de x) 
Bus Red Omega Crossbar 
Latencia constante O(log2P) constante 
Ancho de banda por procesador O(w/P) → O(w) O(w) → O(w × P) O(w × P) 
Complejidad del cableado O(w) O(w × P × logk P) O(w × P2) 
Complejidad de la conmutación O(P) O(P/k × logk P) O(P2) 
Capacidad de comunicación de una en una algunas permutaciones, broadcast... 
todas las 
permutaciones 
Ejemplos Symmetry S-1 Encore Multimax 
BBN TC-2000 
IBM RP3 
Cray Y-MP/816 
Fujitsu VPP500 
 
 
6.4 REDES FORMADAS POR ENCAMINADORES DE 
MENSAJES 
En las redes multietapa que acabamos de analizar, los enlaces entre 
conmutadores se establecen dinámicamente en función de las necesidades de 
comunicación, y los nodos y los dispositivos de la red están "separados". 
Tenemos un segundo tipo de redes, en el que cada nodo de la red utiliza un 
dispositivo propio para gestionar la comunicación, un encaminador de 
mensajes; estas redes se conocen como redes estáticas, Para formar la red, se 
conectan entre sí los dispositivos específicos de gestión de mensajes de cada 
nodo, los encaminadores de mensajes (routers), de acuerdoa una 
determinada topología. Este tipo de redes son las que habitualmente utilizan 
los sistemas paralelos actuales, sobre todo si el número de procesadores que 
se desea conectar es elevado. 
 
 
 
 
 
 
 
Red de 
comunicación 
Procesador y 
memoria local 
 
router 
Gestor de 
comunicaciones 
 
Enlaces de la red 
 
6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 185 ▪ 
6.4.1 Encaminadores de mensajes 
Como ya sabemos, la comunicación entre procesadores en sistemas 
paralelos de memoria distribuida se realiza mediante paso de mensajes. Para 
enviar un mensaje de un procesador a otro se pasa dicho mensaje al 
encaminador de mensajes local, desde donde irá avanzando, encaminador a 
encaminador, hasta el nodo destino. 
La siguiente figura representa, de manera esquemática, un encaminador de 
mensajes típico. Por un lado, tenemos cierto número de puertos de entrada y 
salida, mediante los cuales se va a formar la red, más un puerto específico 
para la conexión con el procesador local. Por otro, un autómata que decidirá 
en cada caso el puerto de salida correspondiente a un mensaje que se ha 
recibido en uno de los puertos de entrada, bien para pasarlo a otro 
encaminador, bien para pasarlo al procesador local. 
 
 
 
 
 
 
 
 
 
 
Es posible que un mensaje quede bloqueado en algún punto intermedio de 
su recorrido, debido a que no esté libre el camino de salida que necesita para 
seguir avanzando. Un poco más adelante concretaremos qué hay que hacer 
en esos casos, aunque es habitual que el encaminador disponga de búferes 
para almacenar mensajes en esas circunstancias. 
Un encaminador de mensajes no es sino un tipo de conmutador algo más 
complejo. El objetivo de ambos dispositivos es el mismo, conectar entradas 
y salidas para abrir un camino a los mensajes; es la organización de la red la 
que los hace algo diferentes. El encaminador de mensajes es un elemento 
más de los nodos que forman la red, no como los conmutadores de una red 
Omega, que son independientes de los nodos de la misma, como 
consecuencia de ello, por ejemplo, en el caso de las redes estáticas no todos 
los mensajes recorren el mismo número de pasos en la red: unos nodos están 
mas cerca que otros. 
Puertos 
entrada 
Puertos 
salida 
Mensajes del 
procesador local 
Mensajes para el 
procesador local 
Enlaces de la red 
de comunicación 
 
Función de routing 
Enlaces de la red 
de comunicación 
 
 
Búferes 
+ 
 
Crossbar 
▪ 186 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
6.4.2 Topologías de red más utilizadas 
Como ya hemos visto, la red ideal es la que conecta todos con todos, un 
crossbar, tal como el de la figura. 
 
 
 
 
 
Es claro que se trata de una topología de difícil aplicación si el número de 
procesadores a conectar es elevado: el número de enlaces crece 
cuadráticamente y el grado de los nodos (número de conexiones) es grande 
(y no es constante). 
Tenemos que analizar por tanto las alternativas más viables y que más se 
utilizan en los sistemas MPP actuales. En general, cada nodo de las 
topologías que vamos a analizar consta de procesador, memoria y 
encaminador de mensajes; los enlaces son siempre bidireccionales. 
 
6.4.2.1 Redes de una dimensión: la cadena y el anillo 
Aunque no es una topología que se utilice en casos reales, analicemos 
como punto de partida dos redes de una sola dimensión: la cadena y el anillo. 
 
 
 
 cadena anillo 
 
El grado de ambas redes es 2 (2 enlaces por nodo). El anillo es regular y 
simétrico, pero la cadena no. La tolerancia a fallos es baja: basta con que 
falle un enlace, cualquiera, para romper la cadena, o dos enlaces en el caso 
del anillo. Los parámetros de distancia (siendo P, el número de 
procesadores, par) son los siguientes (ver apéndice del capítulo): 
 
 diámetro distancia media 
cadena → P – 1 (P + 1) / 3 → P / 3 
anillo → P / 2 P2 / 4(P–1) → P / 4 
 
6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 187 ▪ 
Las redes de una dimensión no son adecuadas para conectar un número 
alto de procesadores. Pero pueden generalizarse fácilmente a 2, 3 o más 
dimensiones. 
6.4.2.2 Mallas y Toros (mesh, torus) 
Las mallas y los toros son las topologías resultantes de generalizar la 
cadena y el anillo respectivamente a n dimensiones. En la figura aparecen 
una malla y un toro de 2 dimensiones. Hemos representado redes cuadradas, 
pero el número de procesadores por dimensión puede ser cualquiera. Para 
obtener un toro a partir de una malla, basta con enlazar entre sí, formando 
anillos, los nodos de los bordes de la malla en cada dimensión (con otro tipo 
de conexión entre los nodos de los bordes, se obtiene otro tipo de redes). 
 
 
 
 
 
 
 
 
En general, se utilizan mallas o toros de n dimensiones (n = 2 o 3), con k 
nodos por dimensión. Por tanto, el número total de nodos de la red es P = kn. 
En el caso de dos dimensiones, P = k × k (si la red no es cuadrada, k1 × k2). 
El grado de ambas redes es 2n (hay dos enlaces por dimensión), un valor 
bajo. El toro es regular y simétrico28, pero la malla no, ya que el grado de 
todos los nodos no es el mismo (por ejemplo, en dos dimensiones, los nodos 
de los vértices sólo tienen 2 enlaces, los de los lados 3, y el resto 4), por lo 
que los nodos no tienen la misma imagen de la red. 
La tolerancia a fallos es alta, ya que hay muchos caminos para ir de un 
nodo a otro. En el caso peor, hay que quitar n enlaces en la malla (los 
enlaces de un nodo de uno de los vértices) y 2n en el toro (en dos 
dimensiones, los cuatro enlaces de cualquiera de los nodos) para romper la 
 
28 Para conseguir la simetría se necesita que todos los enlaces sean de la misma longitud, lo que se 
consigue con la red denominada folded torus. Se propone como ejercicio dibujar nuevamente el toro 
de 4×4, pero con todos los enlaces de la misma longitud. 
0 1 2 3 
 
0 1 2 3 
0 
 
1 
 
2 
 
3 
▪ 188 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
red; por tanto, la arco-conectividad es n y 2n respectivamente. Sin embargo, 
en general, se pueden eliminar (estropear) muchos más enlaces de la red sin 
que ésta pierda la conectividad entre todos sus nodos. Respecto al número de 
enlaces, una malla tiene n × kn-1 × (k–1) enlaces, y un toro n × kn. 
Para dividir la red en dos partes iguales (bisección), es necesario eliminar 
k enlaces en la malla y 2k enlaces en el toro (debido a los anillos). 
Los parámetros de distancia de ambas redes son los siguientes (k par; ver 
apéndice): 
 
 diámetro distancia media 
malla → n × (k – 1) 
 
13
12
−
−
n
n
k
k
k
kn → n × (k / 3) 
toro → n × k / 2 
 
14 −n
n
k
kkn → n × (k / 4) 
 
La distancia máxima de la malla corresponde a ir de un extremo a otro en 
cada dimensión; en el toro, en cambio, la distancia máxima por dimensión 
nunca es mayor que medio anillo. Simplificando el cálculo para el caso en el 
que el número de nodos sea grande, la distancia media en una malla es un 
tercio del número de nodos en cada dimensión, y en un anillo un cuarto. 
 
6.4.2.3 Hipercubos (hypercube) 
Un hipercubo es una malla de n dimensiones que sólo tiene dos nodos por 
dimensión, por lo que también se le denomina n-cubo binario. En las 
siguientes figuras tenemos hipercubos de 1, 2, 3 y 4 dimensiones. Para 
generar un hipercubo de n dimensiones, hay que construir dos hipercubos de 
n–1 dimensiones y enlazar, uno a uno, los nodos de la misma posición en 
cada red. 
 
 
 
 
 
 
 
(000) 
(110) (111) 
(101) 
(011) 
(100) 
(010) 
(001) 
(0) (1) (0000) (0001) 
(0010) (0011) 
(0100) (0101) 
(0110) (0111) 
(1000) (1001) 
(1010) (1011) 
(1100) (1101) 
(1110) (1111) 
(01) 
(10) (11) 
(00) 
6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 189 ▪ 
Cada nodo de la red tiene una dirección de n bits, (xn–1, xn–2, ..., x1, x0), en 
la que cada bit valdrá 1 o 0 en función de la posición que ocupe el nodo en la 
correspondiente dimensión. Así, elnodo (xn-1, xn-2, ..., x1, x0) está conectado 
con los nodos (/xn-1, xn-2, ..., x1, x0), (xn-1, /xn-2, ..., x1, x0), (xn-1, xn-2, ..., /x1, x0), 
(xn-1, xn-2, ..., x1, /x0) [/ = not]; es decir, con aquellos nodos cuya dirección 
difiere únicamente en un bit. Por ejemplo, el nodo 0000 está conectado con 
los nodos 1000, 0100, 0010 y 0001. 
Un hipercubo de n dimensiones tiene 2n nodos. Un hipercubo de P nodos 
es de log2 P dimensiones. El grado de la red es n (o sea, log2 P), la dimensión 
de la misma, y no es constante, ya que crece con el tamaño de la red. El 
hipercubo es simétrico y regular. El número de enlaces es alto, (P/2) × log2 P, 
y en la misma medida es alta también la tolerancia a fallos; la arco-
conectividad es n (hay que eliminar los n enlaces de un nodo dado para 
romper la red). 
La bisección de un hipercubo es de P/2 enlaces (para formar un hipercubo 
de P nodos unimos uno a uno los nodos de dos hipercubos de P/2 nodos). 
Los parámetros de distancia del hipercubo son los siguientes (ver 
apéndice): 
 
 diámetro distancia media 
hipercubo → n n × 2n–1 / (2n–1) → n / 2 
 
La distancia máxima es el número de dimensiones, ya que sólo se puede 
dar un paso en cada dimensión; si el número de nodos es grande, la distancia 
media resulta ser la mitad del número de dimensiones. 
Los hipercubos presentan dos inconvenientes claros. Por una parte, el 
grado no es constante, lo que quiere decir que el número de enlaces del 
procesador (mejor, del interfaz de la red) no es constante, sino que varía en 
función del tamaño de la red. Por tanto, es complicado ampliar la red, ya que 
habría que cambiar todos los elementos de la red (o tener prevista desde el 
principio la posible ampliación del sistema). Además la construcción de 
hipercubos de muchos nodos no es sencilla, ya que el cableado de la red es 
muy denso. Por otra parte, no se puede construir un hipercubo con cualquier 
número de procesadores, sino sólo con potencias de 2. Por ejemplo, 512 o 
1.024 procesadores, pero no 800. 
Topologías tales como mallas, toros e hipercubos pueden englobarse 
dentro de una clase más general, denominada k-ary n-cube. Se trata de redes 
▪ 190 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
de n dimensiones, con k nodos en cada dimensión. Los nodos de cada 
dimensión se unen mediante un anillo (o una cadena) y los enlaces son 
unidireccionales (por ejemplo un toro 4×4 sería un 4-ary 2-cube). 
 
6.4.2.4 Árboles y árboles densos (fat tree) 
Otra topología muy utilizada en los sistemas paralelos MPP es el árbol. 
Un árbol puede tener estructuras diversas; la más utilizada consiste en 
disponer los procesadores en las hojas del árbol y los encaminadores de 
mensajes en el resto de los nodos, tal como aparece en la siguiente figura. 
El grado del árbol es k, el número de nodos que salen de cada nodo (como 
excepción, el grado no representa en este caso el número de enlaces). Así, el 
número de niveles del árbol resulta ser logk P. En la figura se representa un 
árbol binario, con k = 2. 
 
 
 
 
 
 
 
 
 
En principio, la red no es regular, ya que el nodo raíz y las hojas sólo tiene 
dos enlaces, mientras que los intermedios tienen tres. Sin embargo, si los 
procesadores únicamente están en las hojas del árbol, la visión que tiene 
cada uno de ellos de la red es la misma, por lo que podríamos decir que, para 
los procesadores, es regular. 
Considerando el tráfico de paquetes, la red es muy desequilibrada. Por 
ejemplo, todo el tráfico que se genere en la mitad derecha del árbol, y que 
vaya dirigido a procesadores situados en la mitad izquierda, tiene que utilizar 
el encaminador de mensajes de la raíz, zona de la red que va a estar muy 
saturada. Además, si se estropeara un enlace de ese nodo o el propio 
encaminador, la red quedaría inconexa. Para superar ese problema, las redes 
en forma de árbol que se utilizan en la realidad disponen de mayor cantidad 
de recursos —enlaces y encaminadores— según se avanza hacia la raíz, con 
Encaminadores 
Procesadores 
 fat tree o 
árbol denso 
6.4 REDES FORMADAS POR ENCAMINADORES DE MENSAJES ▪ 191 ▪ 
lo que la capacidad de gestionar mensajes es mayor en las zonas en las que el 
tráfico va a ser mayor. A este tipo de árbol se le conoce como árbol denso 
(fat tree). Por tanto, la bisección de un fat tree es P/2. 
Los parámetros de distancia de un árbol son los siguientes (grado k; ver 
apéndice): 
 
 diámetro distancia media 
árbol → 2 logk P 
 
1
2log
1
2
−
−
− k
P
P
P
k
 
 
→ 2 log2 P – 2 (k = 2) 
 
En las redes implementadas mediante conmutadores, como las redes 
Omega, los diferentes componentes que forman la red están perfectamente 
diferenciados —por un lado, los procesadores y, por otro, los 
conmutadores— y no hay una relación directa entre ellos. En el caso de las 
redes estáticas, en cambio, cada procesador (o, tal vez, un conjunto pequeño 
de ellos) utiliza un encaminador de mensajes privado para gestionar la 
comunicación. Los árboles que hemos analizado toman la apariencia de 
redes dinámicas, porque los procesadores sólo están en los nodos hoja del 
árbol; los demás elementos de la red sólo se encargan de gestionar los 
mensajes. De hecho, se puede demostrar que los árboles densos y las redes 
butterfly son isomorfos (“equivalentes”). La única diferencia es que para 
llegar al destino en una red butterfly es necesario atravesar toda la red (la 
distancia siempre es la misma), pero en los árboles no siempre es necesario 
llegar hasta el nodo raíz, porque se puede volver hacia atrás en cualquiera de 
los encaminadores intermedios (y de esta forma hacer el camino más corto). 
 
6.4.2.5 Resumen de topologías 
Acabamos de resumir las características principales de las topologías más 
utilizadas hoy en día en los sistemas paralelos MPP. La siguiente tabla 
resume las principales características topológicas de las redes que hemos 
analizado. Por claridad, hemos simplificado algunos parámetros para el caso 
de un número grande y par de procesadores. 
 
 
▪ 192 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
(P = núm. de procesadores, n = núm. de dimensiones, k = grado de los 
conmutadores o del árbol, o bien número de procesadores por dimensión). 
 
 Proc. Grado Regul./ Simetr. 
Enlaces (×w) 
[Conm.] d media Diámetro Bisecc. 
Arco- 
conec. 
Crossbar P P–1 si P (P–1) 1 1 P2/4 P–1 
Omega P k si P (logk P + 1) [(P/k) logk P] 
logk P logk P -- -- 
Malla P = kn 2n no n kn–1 (k–1) ~ n k/3 n (k–1) kn–1 n 
Toro P = kn 2n si n P ~ n k/4 n k/2 2 kn–1 2n 
Hipercubo P = 2n n (log P) 
si (P/2) log P ~ n/2 n P/2 n 
Árbol 
(fat tree) P k si P logk P 
~ 2 logk P – 
2/(k–1) 2 logk P P/2 1 
 
Para analizar con más claridad los parámetros de distancia, la siguiente 
tabla muestra el diámetro y la distancia media de mallas, toros e hipercubos. 
Tal como veremos a continuación, estos parámetros pueden ser esenciales en 
la latencia de la comunicación. Por ejemplo, en el caso de una máquina de 
1.024 procesadores, la distancia media del hipercubo es 3 veces menor que 
la del toro y 4 veces menor que la de la malla. Pero el principal problema del 
hipercubo es su elevado grado (el número de enlaces). Por ejemplo, para el 
caso de 1.024 nodos, el grado del hipercubo es 10: cada nodo se conecta con 
otros 10; en cambio, en el toro o la malla de dos dimensiones el grado es 
solamente 4. 
 
 
 Nodos (número de procesadores) 
D / d (med.) 16 64 256 1.024 16.384 
Malla 2D 6 / 2,7 14 / 5,3 30 / 10,7 62 / 21,3 254 / 85,3 
Toro 2D 4 / 2,13 8 / 4,06 16 / 8,03 32 / 16 128 / 64 
Hipercubo 4 / 2,13 6 / 3,05 8 / 4,02 10 / 5 14 / 7 
Árbol (binario) 8 / 6,53 12 / 10,19 16 / 14,06 20 / 18 28 / 26 
 
En muchos casos, la topología de la red es de tipo jerárquico: los nodos de 
una determinada red con una topología dada, son a su vez sistemas paralelos 
con otra topología (o tal vez sistemas SMP). Por ejemplo, un sistema puede 
ser un hipercubo hasta por ejemplo 64 procesadores, y a partir deahí un 
árbol en el que los nodos son hipercubos. Una de las jerarquías más 
utilizadas es aquella en la que los nodos son sistemas SMP de 4-8 
procesadores. 
6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 193 ▪ 
6.4.2.6 Los enlaces físicos 
Además de los conmutadores o de los encaminadores de mensajes, la red 
está formada por links o enlaces físicos. Estos enlaces pueden ser de 
diferentes tipos en función de las características de la máquina: 
 
• Largos o cortos. Si los cables son largos, es posible tener más de un 
dato a la vez en diferentes “zonas” del mismo (por ejemplo, en una red 
LAN). Cuando son cortos, sólo admiten un dato. Considerados como 
elementos de transmisión digital, el tiempo de transmisión en un cable 
corto es básicamente el tiempo de carga, necesario para tener el 
mismo valor en ambos extremos del cable, tiempo que crece 
logarítmicamente con la longitud del cable. Cuando los cables son 
largos, en cambio, el tiempo de transmisión es proporcional a la 
longitud. 
 
• "Anchos" o "estrechos”. Los enlaces pueden ser de un bit (serie) o de 
varios bits en paralelo: 4, 8, 16 o más. Cuanto más anchos son, más 
información pueden transmitir simultáneamente, y, por tanto, menor 
será la latencia de un mensaje de tamaño dado. En algunos casos, 
algunos de los bits de los enlaces se utilizan para transmitir 
información de control, y el resto para datos. 
Por otra parte, la comunicación puede ser síncrona (mediante un reloj 
global) o asíncrona (mediante un protocolo específico punto a punto). 
Para indicar la capacidad de transmisión de los enlaces, se utiliza el ancho 
de banda (bandwidth), habitualmente en (Mega) Gigabit/s; por ejemplo, 
enlaces de 10 Gb/s. 
Por último, los enlaces pueden ser de par trenzado de cobre (típicos en 
telefonía), cable coaxial (habitual en redes LAN), o de fibra óptica (cada vez 
más utilizados en las redes de comunicación de alta velocidad). 
 
6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN 
LOS SISTEMAS PARALELOS 
La red no es sino el soporte físico de la comunicación entre procesadores; 
por encima de ella, es necesario construir la lógica adecuada que permita 
llevar los mensajes desde el nodo origen al nodo destino. Para analizar el 
comportamiento de una red de comunicación, además de su topología, es 
necesario tener en consideración muchas otras cuestiones: ¿cómo se organiza 
▪ 194 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
el camino (switching strategy)? ¿por dónde se llega al destino (routing 
algorithm)? ¿hay que usar siempre el mismo camino? ¿cómo avanzan los 
mensajes por la red? ¿qué hay que hacer si el camino está ocupado (flow 
control)? Todas estas decisiones hay que tomarlas teniendo en cuenta que el 
objetivo debe ser que la latencia de la comunicación sea baja y el 
throughput de la red alto. 
6.5.1 Los mensajes 
En función de la estructura del sistema paralelo, la información que se 
intercambia entre los procesadores puede organizarse de formas diversas. Si 
se utilizan variables compartidas, la comunicación se efectuará mediante 
operaciones de tipo rd/wr, es decir, pequeños paquetes de control más los 
datos. Si la comunicación se efectúa mediante paso de mensajes, éstos serán 
más largos y estructurados. Por otra parte, la longitud máxima de los 
mensajes que procesa la red suele estar limitada a un valor máximo. Si hay 
que enviar un mensaje más largo, entonces habrá que dividirlo en varios 
paquetes o unidades de transmisión de tamaño fijo. 
En un paquete es habitual distinguir los siguientes campos: 
• Cabecera: información de control que identifica al paquete, y que 
incluye, junto a la longitud, tipo, prioridad... del paquete, información 
sobre la dirección de destino. 
• Payload o carga de datos: datos a transmitir (contenido del mensaje). 
• Cola: información de control para indicar fin de paquete, códigos de 
detección de errores tipo checksum (más común en las redes LAN que 
en los MPP), etc. 
 
control datos control. 
cola cabecera 
 
Así pues, tenemos que distinguir la información de control y los datos 
dentro de un paquete. Si los enlaces entre encaminadores son muy anchos 
(de muchos bits), los datos y la información de control pueden ir en paralelo. 
Por ejemplo, en el Cray T3D, los enlaces son de 24 bits, 16 bits para datos y 
4 para control (y 4 más para control de flujo). Con los 4 bits de control se 
indica, por ejemplo, el comienzo y el final de la transmisión de un paquete, 
etc. En el Cray T3E, en cambio, la información se envía en paquetes tales 
como los que hemos comentado (se dice que se hace framing). 
6.5 LA COMUNICACIÓN A TRAVÉS DE LA RED EN LOS SISTEMAS PARALELOS ▪ 195 ▪ 
Toda la información de control que se añade a los paquetes (cabecera, 
checksum...) supone una sobrecarga en la comunicación y, por tanto, una 
pérdida de eficiencia, por lo que hay que mantenerla acotada. Por ejemplo, si 
para mandar 100 bytes de datos hay que enviar un paquete con 128 bytes, 
entonces sólo aprovecharemos 100 / 128 = 78% del ancho de banda del 
sistema (para transmitir información útil). 
La anchura de los enlaces de red que se utilizan para transmitir la 
información suele ir desde un bit (transmisión serie, se ha utilizado poco) 
hasta 16 bits (o más); según los casos, por tanto, puede que se necesite más 
de un “ciclo de transmisión” para transmitir “la unidad lógica más pequeña” 
de un paquete. Desde el punto de vista del control, los paquetes se suelen 
dividir en flits. En este contexto, un flit se define como la cantidad mínima 
de información con contenido semántico, normalmente la información 
mínima que se requiere para poder encaminar el paquete. Por ejemplo, si se 
utiliza transmisión en serie, cuando recibimos el primer bit de un paquete no 
podemos decir nada sobre dicho paquete; necesitamos más bits para poder 
saber a dónde va dicho paquete. 
Por tanto, los mensajes/paquetes se miden en flits. Lo más habitual es que 
un flit sean 8 o 16 bits, coincidiendo con la anchura de los enlaces que unen 
los encaminadores de la red. Normalmente, en uno o dos bytes se puede 
codificar la información necesaria para poder encaminar un paquete. De este 
modo, un flit se podrá transmitir entre encaminadores en un solo “ciclo”. 
6.5.2 Patrones de comunicación: con quién y cuándo 
hay que efectuar la comunicación. 
No es posible responder de manera precisa a esa cuestión, ya que las 
necesidades de comunicación, obviamente, dependen de la aplicación a 
ejecutar. Sin embargo, podemos aclarar algunos aspectos. Por una parte, hay 
que tener en cuenta que el esquema de comunicación de una aplicación y el 
esquema de comunicación en la red son dos cosas diferentes, ya que en 
medio se encuentra la asignación física de procesos a procesadores. Es decir, 
si los procesos P1 y P2 tienen que intercambiar información, no es lo mismo 
que se asignen a procesadores contiguos en la red que asignarlos a 
procesadores en dos extremos de la red. Por otra parte, las necesidades de 
comunicación no suelen ser siempre de todos con todos, sino entre algunos 
de los procesos. Tenemos por tanto que considerar múltiples aspectos: las 
▪ 196 ▪ Capítulo 6: LA RED DE COMUNICACIÓN DE LOS SISTEMAS PARALELOS 
 
necesidades de comunicación de la aplicación, el nivel de paralelismo, el 
reparto de procesos, etc. 
Al esquema de comunicación que hay que ejecutar en la red se le conoce 
como patrón de comunicación. El patrón de comunicación puede ser 
espacial o temporal, es decir, puede indicar la distribución espacial de los 
paquetes (a dónde van) o su distribución en el tiempo (cuándo se 
transmiten). Analicemos algunos de los casos más típicos. 
 
1. Aleatorio 
 Uno de los patrones que más se utiliza para analizar el 
comportamiento de las redes es el patrón aleatorio, tanto en el espacio 
como en el tiempo. La comunicación es aleatoria si la probabilidad de 
enviar un paquete del nodo i al j, PrCij, es la misma para cualquier par 
de nodos de