Documentos de Académico
Documentos de Profesional
Documentos de Cultura
ComPar 1
ComPar 1
PARALELOS
Computación de Alta Velocidad
A. Arruabarrena — J. Muguerza
A. Arruabarrena — J. Muguerza
septiembre 2012
ÍNDICE
Introducción ......................................................................... 1
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,
1000, 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
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 ▪
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
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 ▪
¿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.
Unidades
Registros funcionales
Memoria
Procesador
escalar (op.)
(completo)
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.
1.2 DEPENDENCIAS DE DATOS ▪ 15 ▪
LV/SV → BD L AM M M M E
ADDV → BD L A A E
do i = 0, N-1 LV V1,A(R1)
A(i) = A(i) + 1 → ADDVI V2,V1,#1
enddo SV A(R1),V2
LV V1,A(R1) BD L AM M M M E ... E
ciclos ← 6 → ← N → ← 3 → ← N → ← 4 → ← N
ciclos ← 6 → ← 3 → ← 4 → ← N →
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 ▪
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 ▪
o, esquemáticamente:
LV
ADDVI T ~ 2N
SV
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áquina cuenta con un solo bus de memoria, y en el otro caso cuenta con
dos buses.
LV LV
LV LV
ADDV ADDV
SV SV
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
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
nm ≥ t m
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:
nm
MCD(nm, s ) (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:
nm
≥ tm
MCD(nm, s )
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 -
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
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]
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 ▪
2 buses / encadenamiento
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 ▪
MOVI VS,#1
MOVI R1,#N
do i = 0, N-1 mas: MOV VL,R1
A(i) = A(i) + 1 → LV V1,A(R2)
enddo 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
N
TV = (ti + tbuc ) + tv N
Lmax
▪ en modo vectorial 7
TV = ti + tv N ti = tiempo de inicio
tv = tiempo para procesar un elemento del vector
250
TV
200
pendiente = tb
150
2ti
100
TV = 30 + 2N
5
ti N1/2
0
0 25 50 75 100 125 150
N N
RN = = × OpCF × F Mflop/s (TV en ciclos, F en MHz)
TV ti + tv N
R∞
(rendimiento)
R∞/2
R
N1/2
N (número de elementos)
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.
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
¡
TE t N te
KV = = e K∞ =
T V ti + t v N tv
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
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.
ti N1/ 2
t e N v = ti + t v N v → Nv = =
te − tv K ∞ − 1
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.
TVE = f TV + (1 – f) TE
TE TE TE K
KVE = = = =
TVE fTV + (1 − f )TE TE f (1 − K ) + K
f + (1 − f )TE
K
1.4 VELOCIDAD DE CÁLCULO DE LOS COMPUTA-DORES VECTORIALES ▪ 35 ▪
16
KV = ∞
N N N N
RVE = = = =
TVE fTV + (1 − f )TE f (ti + tv N ) + (1 − f )te N f (ti + tv N ) + (1 − f ) K ∞ tv N
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).
14 Ley de Amdahl
12
tv = 5 ns
te = 66,6 ns
10
tv = 10 ns
4
te = 33,3 ns
CRAY X-MP
2
tv = 10 ns
te = 66,6ns
0
0 0.2 0.4 0.6 0.8 1
f (factor de vectorización)
1
1: A = B + C
B
2: B = D
2
j
A, (0, 1)
A, (0, 1)
do i = 2, N-1 1
do j = 1, N-2 i A, (2, –1)
1.5.2 Vectorización
do i = 0, N-1
A(i) = B(i) + C(i)
enddo
LV V1,B(R1)
LV V2,C(R1)
ADDV V3,V1,V2
SV A(R1),V3
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 ▪
i
1
do i = 0, N-1 A, 0
1: A(i) = B(i) + C(i) A, 0
2: D(i) = A(i)
enddo 2
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
1 i
do i = 1, N-1
1: A(i) = B(i) + C(i)
A, 1
2: D(i) = A(i-1)
enddo A, 1
2
1 δ< 2
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
1
do i = 0, N-2 i
1: A(i) = B(i) + C(i)
A, 1
2: D(i) = A(i+1)
enddo 2 A, 1
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
do i = 3, N-1
1: A(i) = A(i-3) * 3 A, 3 1
enddo
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.
▪ 46 ▪ Capítulo 1: COMPUTADORES VECTORIALES
MOVI VL,#N-1
1 MOVI VS,#1
do i = 1, N-1 B, 0
LV V1,B+1(R1)
A(i) = B(i) SV A+1(R1),V1
B(i) = B(i-1) 2
MOVI R3,#N-1
enddo B, 1
buc: FLD F1,B(R2)
FST B+1(R2),F1
(puede vectorizarse la primera instrucción, ADDI R2,R2,#1
pero no la segunda, debido a la dependencia, una SUBI R3,R3,#1
recurrencia) BNZ R3,buc
do i = L1, L2
X(f(i)) = X(g(i)) + 1
enddo
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.
ai+b
ci+d
L1 i1 i2 L2
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.
▪ 48 ▪ Capítulo 1: COMPUTADORES VECTORIALES
i i i
L1 L2 L1 L2 L1 L2
(1) (2) (3)
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.
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
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 se realiza, 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.
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
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
j = 2
k = 2
do i = 1, L
j = j + 5
R(k) = R(j) + 1
k = k + 3
enddo
i= 1 2 3 4 5 ...
j= 7 12 17 22 27 ...
k= 2 5 8 11 14 ...
▪ 52 ▪ Capítulo 1: COMPUTADORES VECTORIALES
j=5i+2 y k=3i–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
do i = 0, N-2 1
1: A(i) = B(i) + C(i)
2: D(i) = A(i) + A(i+1) A, 0 A, 1
enddo
2
0
do i = 0, N-2
A, 1
0: [T(i)] = A(i+1)
1: A(i) = B(i) + C(i) 1 T, 0
2: D(i) = A(i) + [T(i)]
A, 0
enddo
2
1
do i = 0, N-3
1: A(i) = B(i) + C(i) A, 0 A,2
2: A(i+2) = A(i) * D(i)
enddo 2
1
do i = 0, N-3 T, 0
1: [T(i)] = B(i) + C(i)
2 T, 0
2: A(i+2) = [T(i)] * D(i)
3: A(i) = [T(i)] A, 2
enddo 3
MOVI VL,#N-2
MOVI VS,#1
(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
do i = 0, N-1 i
do j = 1, N-1 1
A(i,j) = A(i,j-1) + 1
A, (0, 1)
enddo
enddo
do j = 1, N-1 i
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.
j
do i = 1, N-1
1 i
do j = 1, N-2
(1) A(i,j) = B(i-1,j+1) + 1
A, (0, 1)
(2) B(i,j) = A(i,j-1)
enddo 2
enddo B, (1, -1)
do i = 1, N-1 i
1 A, (1, 0)
do j = 1, N-1 B
(1) A(i,j) = A(i-1,j) + 1
A
(2) B(i,j) = B(i,j-1) * 2
2
enddo
B, (0, 1)
enddo
do i = 0, 99
do j = 0, 9
A(i,j) = A(i,j) + 1
enddo
enddo
do i = 0, N-1
suma(i) = A(i) + B(i)
C(i) = suma (i) * suma (i)
D(i) = suma (i) * 2
enddo
do i = 0, N-1 do i = 0, N-1
Z(i) = X(i) + Y(i) Z(i) = X(i) + Y(i)
enddo R(i) = Z(i) + 1
enddo
do i = 0, N-1
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 do i = 1, L
Z(i) = X(i) + Y(i) Z(i) = X(i) + Y(i)
enddo ≠ R(i) = Z(i+1) + 1
do i = 1, L enddo
R(i) = Z(i+1) + 1
enddo
do i = 0, N-1
if (B(i) > 5) then A(i) = A(i) + 1
enddo
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
s=3
14
1 8
CVI V1,R1 Genera un vector de índices, con los valores 0, R1, 2R1, ...,
(Lmax–1)R1.
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 ▪
MOVI VL,#M
MOVI R1,#1
CVI V4,R1 ; 0, 1, 2, 3...
MULV V5,V4,V4 ; registro de índices: i*i
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.
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
1.6 RESUMEN
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.
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 una 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
5120 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 8192 procesadores). En la lista citada (junio 2002) había 41
computadores vectoriales.
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)
Array de cálculo
▪ 72 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos)
P0 P1 Pp–1
Red de comunicación
sistema
M0 Mm–1 E/S
Memoria principal
▪ 74 ▪ Capítulo 2: COMPUTADORES PARALELOS (conceptos básicos)
P0 Pp–1
E/S E/S
M M
K K
Red de comunicación
2.3 COMPUTADORES MIMD ▪ 75 ▪
Tp
Tcom
Tej
Núm. procesadores
fa = Ts / Tp
efic = fa / P (habitualmente en %)
fa = Ts / (Ts / P) = P
efic = fa / P = 1
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)
∑f
Ts
Tsp = f × Tp + (1–f) × Ts (en general, Tsp = i )
i =1
i
fa = Ts / Tp = P / [ P (1–f) + f ]
(1–f) Ts f Ts
1 procesador
(1–f) Ts f Ts × P
en paralelo en paralelo
(1–f) Ts f Ts / P (1–f) Ts f Ts
P procesadores
Ts = (1–f) Ts + f Ts P
Tp = (1–f) Ts + f Ts = Ts → fa = Ts / Tp = (1–f) + f P
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 1024 MB/s para acceder a las
variables compartidas, el máximo que admite el bus.
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:
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.
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.
P1 P2 P1 P2
wr A,#3 wr A,#3
A = 4→3? A = 4→3?
MP MP
Invalidación Actualización
17 Aunque en el ejemplo sólo aparece una palabra, un bloque contiene siempre varias palabras.
▪ 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.
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.
Para definir los estados del bloque basta con usar tres bits. Por ejemplo:
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:
I, - S BR M BR,INV
S S M INV S I
acierto
M M M S BW I BW
Tráfico (datos)
MP → MC: BR // I → S, M
MC→ MP: BW // M → S, I (+reemplazo)
M
BR (BW)
PR - BR INV (BW)
PW (INV)
PW (BR,INV) INV
PR (BR)
I, -
3.2 PROTOCOLOS DE COHERENCIA SNOOPY ▪ 95 ▪
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.
I, - BR M BR,INV
sh: S
E E M S I
acierto
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
PR - PW
PW PW (INV)
PR PR - BR
E BR S
nsh sh
INV INV
PW (BR,INV) PR (BR)
I, -
I, - S BR M BR,INV
S S M INV S I
acierto
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)
PW (INV)
PR - PW PR - BR
M O
BR
PW (BR,INV) PR (BR)
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:
- BR
sh: S sh: S BR,BC
E E M S S
E
acierto
nsh:
S S BC S S
sh: 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)
PR - PW
M nsh
PW (BR) (-)
BR (BW)
sh
PW (BC)
PW (BC)
PW (BC) nsh
sh
PR E S PR
BR BR - BC
nsh sh
PR (BR)
(-)
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.
- sh: S
BR
sh: O BR,BC
E E M S S
nsh: M -
S S BC S S
sh: O
acierto
M M M O S
nsh: M -
O O BC O S
sh: O
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) ]
(-)
PW (BR)
nsh sh
(BC)
nsh
PW
PW sh (BC)
PR - PW M O
BR PR - BR
BC (BC) BC
nsh sh
PW
PW
PR
PR E BR
S BR - BC
nsh sh
PR (BR)
(-)
to controller
compar.
to controller
compar.
system bus
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
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 ▪
• 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 en el 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.
PR - PW
M
BGR (BR,INV) BR (BW)
INV (BW)
BGR (INV)
IM
INV SM
PW
PW (BRQ)
PR E BR S PR - BR
nsh sh
BGR (BR)
INV INV
ISE
PW (BRQ) PR (BRQ)
I, -
SMP
P
C snoopy local
B1
MP K K MP
B2
hardware para la
coherencia global
el bloque a quien lo solicitó (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).
MC MC MC MC
M→S E→S
M→S
E→S
B2 1 a 4
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.
3.4 SNOOPY JERÁRQUICO ▪ 117 ▪
N1 N2 N3
MC MC MC MC MC MC
wr A
S→M S→I
S→I S→I
MP MP MP
INV A INV A INV A A
B1 B1 B1
1 3
KL KR KL KR KL KR
4.1 INTRODUCCIÓN
P1 P2
... ...
ST A,F1 ...
... ...
... LD F4,A
P1 P2
... ...
LD R1,CONT LD R1,CONT
ADDI R1,R1,#1 ADDI R1,R1,#1
ST CONT,R1 ST CONT,R1
... ...
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 ▪
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:
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):
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
t0 = k t1 = k c t 2 = k c2 ... (c > 1)
unlock: ST CER,R0
RET
unlock: ST CER,R0
RET
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;
unlock: ST CER,R0
RET
RET
unlock: ST CER,R0
RET
Fetch&Incr R1,CONT
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ómicas para hacerlo.
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:
esp: LD R2,TURNO
SUB R3,R1,R2
BNZ R3,esp ; esperar turno
RET
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 ▪
P2 LD x BRQ. . . . . LD . . . . . . . . . . . . . LD . . . . . . . . .
P3 LD x BRQ. . . . . . . . . . . . LD . . . . . . . . LD . . . . . . . . .
P4 LD x BRQ. . . . . . . . . . . . . . LD . . . LD . . . . . . . . .
P1 (productor) P2 (consumidor)
X = F1(Z); while (aviso==0) {};
aviso = 1; 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.)
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
};
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);
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 con el 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
4.5 RESUMEN
5.1 INTRODUCCIÓN
P1 P2
A = 1; (wr1) print B; (rd1)
B = 2; (wr2) print A; (rd2)
P1 P2
A = 1; ...
tiempo
... print B;
B = 2; ...
... print A;
P1 P2
A = 1; (wr1) while (LISTO == 0) {}; (rd1)
LISTO = 1; (wr2) print A; (rd2)
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.
5.1 INTRODUCCIÓN ▪ 153 ▪
P1 P2
F1 = 1; F2 = 1;
if (F2 == 0) then if (F1 == 0) then
< código > < código >
... ...
P1 P2
A = 1; (wr1) while (LISTO == 0) {}; (rd1)
LISTO = 1; (wr2) print A; (rd2)
▪ 154 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS
P1 P2 P3
A = 1; while (A == 0) {};
B = 1; while (B == 0) {};
C = A;
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 ▪
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
En conjunto
P1 P2
SC si SC no
a c a a a c
tiempo b d b c d d
c b c b
d d b a
instrucción b
P P P P
orden
atomicidad
MEM
2. ACK 2. ACK
P1 P2 P3 P4
A = 2; A = 3; while (B ≠ 1) {}; while (B ≠ 1) {};
B = 1; C = 1; while (C ≠ 1) {}; while (C ≠ 1) {};
reg1 = A; reg2 = A;
2. ACK 2. ACK
3. seguir 3. seguir
P1 P2 P1 P2
A = 1; A = 0; r1 = 1; A = 0;
B = A; A = r1;
B = r1;
(2 ST / 1 LD) (2 ST)
P P P P
LD ST ST ST ST
búferes ST
(FIFO)
MEM
P1 P2
X = nuevo_valor; Y = nuevo_valor;
Y_copia = Y X_copia = X
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:
P1 P2 P1 / P2 / ... / Pn
...
X = X + 1; ... lock(cer);
Y = B + 1; while (flag == 0) {}; yo = i;
flag = 1; A = X / 2; i = i + N;
... B = Y; j = j - 1;
unlock(cer);
...
SC todas
WO todas SYNC
sa >> w/r
REL, ACQ,
RC w/r >> sr RMW
s >> s
▪ 166 ▪ Capítulo 5: CONSISTENCIA DE LA MEMORIA EN LOS SISTEMAS PARALELOS
SC TSO/PC PSO WO RC
wr,rd,s >> wr,rd,s – wr >> rd – wr >> wr – rd >> wr, rd
rd = A = A = A = A = A
wr B = B = B = B = B =
wr C = C = C = C = C =
rd = D = D = D = D = D
wr E = E = E = E = E =
wr F = F = F = F = F =
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 ▪