Un programa se compone de instrucciones.
1 x=A //Lee el valor de A
2 B=2 //Escribe un valor en B
3 y=B //Lee el valor de B
Usaré las letras mayúsculas (A,B,C) para las localizaciones de memoria y las minúsculas (a,b,c) para registros o valores que no estén en memoria. Como nos interesa el modelo de memoria, ignoraremos las operaciones que no trabajen con memoria. Para mayor facilidad usaremos números para cada instrucción.
Cuando se ejecuta un programa las instrucciones que se van ejecutando conforman una secuencia que se denomina traza. Se llama orden de programa a la relación de orden estricto que encadena las instrucciones según se ejecutan en una traza. Si escribimos esta relación como [$ \rightarrow_p $] entonces, el programa de arriba tiene la siguiente traza
[$$ 1 \rightarrow_p 2 \rightarrow_p 3 $$]
Nada extraño. Esta relación es un orden estricto y, en principio, total.
Modelo secuencialmente consistente (SC)
Cuando tenemos varios procesadores, cada uno de ellos ejecutando una hebra distinta, ¿cómo se reparten las instrucciones? ¿Y qué instrucciones se ejecutan antes que otras? Un modelo de memoria compartida responde a estas preguntas.
El modelo más simple es el secuencialmente consistente (SC) de Lamport. En este modelo las instrucciones de todos los procesadores se intercalan y el orden resultante de las mismas ha de ser un orden total que llamaremos de ocurrencia [$ \rightarrow_o $].
Si no pusiéramos más condiciones, este orden podría ser cualquiera, incluso sin respetar el orden de programa.
Por ejemplo, este programa con dos hebras
INICIALMENTE: A=B=0
HEBRA 1 HEBRA 2
1 x=A 11 y=B
2 B=1 12 A=1
Tiene el siguiente orden de programa
[$$ 1 \rightarrow_p 2 $$]
[$$ 11 \rightarrow_p 12 $$]
Sin condiciones adicionales, el orden de ocurrencia podría ser, por ejemplo
[$$ 2 \rightarrow_o 12 \rightarrow_o 1 \rightarrow_o 11 $$]
Nota: Tanto el orden [$ \rightarrow_p $] como el orden [$ \rightarrow_o $] son transitivos por lo que con el ejemplo de arriba también tendríamos [$ 2 \rightarrow_o 1, 2 \rightarrow_o 11, 12 \rightarrow_o 11 $]. Es usual no listar estas relaciones que se pueden derivar fácilmente de la primera.
Este orden daría como resultado x=y=1 lo cual es bastante contraintuitivo. El modelo SC tiene la siguiente restricción: si [$N$] y [$M$] son dos instrucciones y [$ N \rightarrow_p M $] entonces debe darse que [$ N \rightarrow_o M $]. Esta condición se expresa de la siguiente manera.
[$$ \frac{N \rightarrow_p M}{N \rightarrow_o M} $$]
Esto quiere decir que si en una hebra se ejecuta una instrucción antes que otra por el orden de programa (por ejemplo, la 11 antes de la 12) entonces ese mismo orden aparece en el orden de ocurrencia. Como en el ejemplo anterior teníamos que 12 ocurría antes que 11, excluimos este posible orden del modelo SC.
[$ 2 \rightarrow_o 12 \rightarrow_o 1 \rightarrow_o 11$] no es SC porque [$2 \rightarrow_o 1$] pero [$1 \rightarrow_p 2$]
[$ 11 \rightarrow_o 1 \rightarrow_o 2 \rightarrow_o 12 $] sí es SC y el resultado es un intuitivo x=y=0
[$ 1 \rightarrow_o 2 \rightarrow_o 11 \rightarrow_o 12 $] sí es SC y el resultado es otro intuitivo x=0, y=1
El dibujo típico del modelo secuencialmente consistente es una memoria compartida asignada únicamente a un procesador en cada momento.
Optimizaciones en compiladores y procesadores
Desafortunadamente las cosas no son tan sencillas. Generalmente el orden de programa es excesivamente restrictivo para realizar optimizaciones. Una optimización típica es la siguiente.
1 A=5
2 x=A
3 B=1
En el anterior programa, la instrucción 2 ha de esperar a que la instrucción 1 escriba el valor 5 en A. Un compilador o un procesador astuto reordena el programa de la siguiente forma.
11 A=5
12 B=1
13 x=A
En este programa transformado, la instrucción 12 se ejecuta mientras la instrucción 11 se está terminando. Esto evita que tengamos que esperar en la instrucción 13 tanto como hacíamos en la instrucción 2.
El problema es que el programador ha escrito las instrucciones 1,2,3 y no las 11,12,13. ¿Qué pasa si ejecutamos esto con otra hebra?
HEBRA 1 HEBRA 2
1 A=5 21 if(B=1)
2 x=A 22 A=7
3 B=1
Pensando en SC de ninguna manera podría ocurrir que x=7 ya que para cuando B=1, x ya tiene el valor 5 y por mucho que modifiquemos A=7, x ya no va a cambiar. Esto no es así si el procesador o el compilador reordena el programa de la hebra 1 sin que lo sepamos.
HEBRA 1 HEBRA 2
11 A=5 21 if(B=1)
12 B=1 22 A=7
13 x=A
Aunque el programador ha escrito 1,2,3, el compilador o el procesador lo reordena a 11,12,13 para optimizar la espera de la instrucción 2 tras la 1. Ahora bien, ¡esto ahora permite que x=7! El orden de ocurrencia para este bug sería
[$$ 11 \rightarrow_o 12 \rightarrow_o 21 \rightarrow_o 22 \rightarrow_o 13 $$]
Si el pobre programador intenta razonar sobre el orden del programa original 1,2,3 lo que vería sería
[$$ 1 \rightarrow_o 3 \rightarrow_o 21 \rightarrow_o 22 \rightarrow_o 2 $$]
que no es SC porque la instrucción 3 se ejecuta antes que la 2.
Relajando el modelo SC
Por esta razón el modelo SC no es útil: no permite al compilador o al procesador optimizar los programas sin dejar al programador con un difícil bug a resolver. La solución es exponer al programador un modelo más débil que se denomina un modelo relajado.
¿Cómo vamos a relajar ese modelo? Hay varias formas.
La primera es permitir reordenar las instrucciones de un programa y proporcionar al programador herramientas para decirle al compilador y al procesador cuándo no se pueden reordenar. Una de estas herramientas es una instrucción llamada barrera de memoria (memory fence) y lo único que significa es que es una barrera que las instrucciones no se la pueden saltar al ser reordenadas.
Con una barrera, el programa anterior quedaría así
HEBRA 1 HEBRA 2
1 A=5 21 if(B=1)
2 x=A 22 A=7
3 FENCE
4 B=1
Y ni el compilador ni el procesador podrían reordenar la instrucción 4 sobre la 3 o la 1 o la 2 bajo la 3. De esta manera, el programador se asegura de que leemos A antes de modificar B. Por otra parte, el compilador o el procesador son libres de realizar otras optimizaciones en otros sitios.
¿Pero en qué sitios? ¿Podría reordenar la instrucción 2 sobre la 1? Realmente no, porque necesitamos conocer A en la instrucción 2 y eso depende de la instrucción 1. A esta relación se la llama una dependencia y la escribiremos [$\rightarrow_d$]. Básicamente indica que un valor no se puede usar antes de que haya sido calculado. En un modelo de memoria relajado al menos se debe conservar la dependencia. Es decir,
[$$\frac{N \rightarrow_d M}{N \rightarrow_o M}$$]
Además, las barreras de memorias ordenan lo que ocurre antes y después de ellas.
[$$\frac{N \rightarrow_p M, N\ es\ FENCE}{N \rightarrow_o M},\ \ \frac{N \rightarrow_p M, M\ es\ FENCE}{N \rightarrow_o M}$$]
Entonces, en el programa anterior teníamos.
[$$1\rightarrow_p 2\rightarrow_p 3\rightarrow_p 4,11\rightarrow_p 12$$]
[$$1\rightarrow_d 2,11\rightarrow_d 12$$]
[$$3\ es\ FENCE$$]
Por la dependencia debe cumplirse que
[$$1\rightarrow_o 2,11\rightarrow_o 12$$]
y por barrera debe cumplirse que
[$$1\rightarrow_o 3,2\rightarrow_o 3,3\rightarrow_o 4$$]
Entonces, está claro que
[$1\rightarrow_o 11\rightarrow_o 2\rightarrow_o 12\rightarrow_o 3\rightarrow_o 4$] sí cumple con el modelo relajado
[$1\rightarrow_o 11\rightarrow_o 2\rightarrow_o 12\rightarrow_o 4\rightarrow_o 3$] no cumple con el modelo relajado porque [$4\rightarrow_o 3$]
Nota: La instrucción 12 no se ejecuta, pero de ejecutarse lo haría en la posición arriba puesta.
Nota: Sabemos que si [$4\rightarrow_o 3$] entonces no se da [$3\rightarrow_o 4$] porque estamos tratando con órdenes.
Otras barreras, otros modelos, otras instrucciones
Algunos compiladores y procesadores se restringen a reordenar instrucciones que son de cierto tipo. Los más usuales son
- Reordena una escritura seguida de otra escritura (WW)
- Reordena una lectura seguida de una escritura (RW)
- Reordena una escritura seguida de una lectura (WR)
- Reordena una lectura seguida de otra lectura (RR)
- Impida reordenar escrituras anteriores a la barrera con escrituras posteriores (WW)
- Impida reordenar lecturas anteriores a la barrera con escrituras posteriores (RW)
- Impida reordenar escrituras anteriores a la barrera con lecturas posteriores (WR)
- Impida reordenar lecturas anteriores a la barrera con lecturas posteriores (RR)
Procesador o
Modelo de memoria Relaja WR Relaja WW Relaja RW Relaja RR
SC
IBM 360 Sí
TSO Sí
PC Sí
PSO Sí Sí
WO Sí Sí Sí Sí
RCsc Sí Sí Sí Sí
RCpc Sí Sí Sí Sí
Alpha Sí Sí Sí Sí
RMO Sí Sí Sí Sí
PowerPC Sí Sí Sí Sí
Para más información http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.106.5742&rank=1
0 comentarios:
Publicar un comentario