Arquitectura e Ingeniería de Computadores Examen final de junio Jueves, 21 de junio de 2007 ¡Acuérdate de poner tu nombre en todas las hojas que utilices! ¡Justifica claramente todas tus contestaciones! Utiliza grupos de folios separados para responder a las cuestiones de cada parte SOLUCIONES PARTE ARQUITECTURAS MONOPROCESADOR 1. (2 puntos) Tenemos un procesador superescalar homogéneo de grado 2 (en todas las etapas se pueden manejar hasta 2 instrucciones por ciclo), con búsqueda alineada y emisión alineada y en orden. Para mantener la consistencia secuencial, así como para renombrar los registros y gestionar las interrupciones se utiliza un ROB. Existe una estación de reserva para cada unidad de ejecución y el predictor de saltos es estático y predice que no va a saltar en los saltos hacia adelante, y que si va a saltar en los saltos hacia atrás. Se dispone de las siguientes unidades de ejecución: dos unidades de carga/almacenamiento (segmentadas) de latencia 2 (calcula la dirección en el primer ciclo y accede a la cache en el segundo ciclo), dos ALUs enteras de latencia 1, un sumador en coma flotante (segmentado) de latencia 3, un divisor en coma flotante (segmentado) de latencia 4, y una unidad de saltos que resuelve los saltos en la etapa de ejecución. Suponemos que en el cauce de enteros están implementados los cortocircuitos y reenvíos habituales, pero no así en el cauce de coma flotante. Supón para este problema un tamaño ilimitado del ROB y de cada una de las estaciones de reserva. En dicho ordenador se va a ejecutar la siguiente secuencia de instrucciones que calcula la división de dos vectores x e y componente a componente: addi r2, r0, r0 lw r3, n slli r4, r3, #2 inicio: lf f0, x(r2) lf f1, y(r2) divf f2, f0, f1 sf f2, z(r2) addui r2, r2, #4 sub r5, r4, r2 bnez r5, inicio trap #0 <sgte.> ; ; ; ; ; ; ; ; ; ; ; ; ; el valor inicial de n es bastante grande r2 = 0 (índice) r3 = n r4 = n * 4 (final del vector) f0 = x(i) f1 = y(i) f2 = x(i) / y(i) z(i) = x(i) / y(i) r2 = r2 + 4 (incremento el desplazamiento) compruebo si he llegado al final saltar a inicio si r5 es distinto de 0 fin del programa suponemos las instrucciones siguientes enteras Nota: La instrucción trap no causa la terminación del programa hasta que se retira del ROB. Considera que la primera vez que se accede a memoria (para obtener el valor de n y para obtener el valor de los vectores a partir de r2 ) se produce un fallo de cache L1 que se resuelve en la cache L2 con una latencia total de 3 ciclos. El resto de accesos a memoria son servidos por la cache L1 gracias a la técnica del prefetching. 1 a) (1 punto) ¿En qué ciclo se confirma la primera instrucción de salto? ¿En que ciclo se confirma la segunda instrucción de salto? Dibuja el diagrama correspondiente para justificar tu respuesta. En dicho diagrama indica, para cada instrucción y ciclo de reloj, qué fase de la instrucción se está ejecutando. b) (0,5 puntos) ¿Cuántas instrucciones tendrá el ROB al acabar el ciclo en que se confirma la primera instrucción ejecutada (addi r2, r0, r0)? Para justificar la respuesta, muestra el contenido del ROB en dicho ciclo. c) (0,5 puntos) Considera ahora que el procesador tiene un reloj de 1 GHz. y que el régimen estacionario de ejecución es igual al de la segunda iteración. ¿En cuanto tiempo (en segundos) se ejecutaría el código anterior para un tamaño del vector de 1000 elementos? No hace falta que tengas en cuenta el tiempo necesario para el llenado/vaciado del pipeline. ¿Cuantos MFLOPs obtendríamos para el procesador descrito ejecutando el código anterior? Solución a) Para obtener las dos preguntas de este apartado tenemos que simular la ejecución del programa durante las dos primeras iteraciones. Obtenemos el siguiente diagrama instrucciones–tiempo: 1 addi r2, r0, r0 lw r3, n slli r4, r3, #2 lf f0, x(r2) lf f1, y(r2) divf f2, f0, f1 sf f2, z(r2) addui r2, r2, #4 sub r5, r4, r2 bnez r5, inicio lf f0, x(r2) lf f1, y(r2) divf f2, f0, f1 sf f2, z(r2) addui r2, r2, #4 sub r5, r4, r2 bnez r5, inicio trap #0 2 3 IF IS EX IF IS EX IF IS IF IS IF IF 4 5 WB L2 EX IS IS IF IF C L2 L2 EX IS IS IF IF 6 7 8 L2 L2 EX EX IS IS IF IF WB EX L2 WB IS IS IF IF C WB WB L1 EX EX EX IS IS IF IF 9 10 11 12 13 14 15 16 17 18 19 20 21 C C WB WB EX L1 L1 EX IS IS IF IF C EX WB WB EX IS XX EX EX WB EX - EX EX WB EX EX EX - WB EX - C C(S1) - C - C - - - WB - - - - - C C - C C - C(S1) C - C - C En el diagrama denotamos con L1 un acceso a memoria resuelto en la cache L1 y con L2 un acceso a memoria satisfecho por la cache L2. Como podemos apreciar, los dos primeros accesos a cache fallan en la L1, siendo resueltos en tres ciclos por la cache L2. El tercer acceso a memoria, aunque realiza la etapa de ejecución (EX), se queda bloqueado por la falta de unidades funcionales (solo hay dos unidades segmentadas). Cuando desaparece el riesgo estructural, el acceso se resuelve en L1 pues el dato buscado (y(r2)) está en el mismo bloque de cache que el anterior acceso (x(r2)). A partir del diagrama anterior vemos que la primera instrucción de salto se confirma en el ciclo 17, mientras que la segunda instrucción de salto lo hace en el ciclo 20. b) La primera instrucción ejecutada se confirma en el ciclo 5. Al final de dicho ciclo el contenido del ROB es el siguiente: 2 Entrada 1 2 1 1 5 6 7 ocupado Si Si Si Si Si Si Si instr. lw r3, n slli r4, r3, #2 lf f0, x(r2) lf f1, y(r2) divf f2, f0, f1 sf f2, z(r2) addui r2, r2, #4 estado L2 IS L2 EX IS IS IS dest. r3 r4 f0 f1 f2 Mem[z(r2)] r2 valor No hemos rellenado el campo valor de ninguna entrada porque no hay ninguna instrucción que haya hecho la etapa de escritura. El número total de entradas del ROB ocupadas es de 7 al final del ciclo 5. c) Del diagrama del apartado a) podemos ver que cada iteración del bucle en el régimen estacionario tarda en ejecutarse 4 ciclos de reloj (medido como la diferencia entre el comienzo de las instrucciones de carga en el régimen normal). Por lo tanto, el tiempo que tardan en ejecutarse 1000 iteraciones, sin tener en cuenta los ciclos del llenado/vaciado del pipeline, es el siguiente: T iempo = 4 ciclos ∗ 1000 = 4000 ciclos = 4 µseg Para el cálculo de los MFLOPs, tenemos que en cada iteración sólo realizamos una operación en coma flotante (la división), por lo que obtendríamos: MFLOPs = 1 op / 4 ciclos @ 1 Ghz = 250 MFLOPs. ¤ 2. (2 puntos) En este problema queremos ejecutar el mismo código del problema anterior pero ahora en un procesador superescalar planificado estáticamente con grado de emisión de dos y con las mismas características hardware del problema anterior (salvo que no tiene predictor de saltos y el cálculo del salto se realiza en la etapa ID). Al igual que antes, considera que la primera vez que se accede a memoria (para obtener el valor de n y para obtener el valor de los vectores a partir de r2 ) se produce un fallo de cache L1 que se resuelve en la cache L2 con una latencia total de 3 ciclos. El resto de accesos a memoria son servidos por la cache L1 gracias a la técnica del prefetching. Se pide: a) (0,75 puntos) ¿Cuantos ciclos tarda en ejecutarse las dos primeras iteraciones del bucle sin planificar el código (teniendo en cuenta el código de inicialización)? Compáralo con el tiempo obtenido en el caso del procesador superescalar planificado dinámicamente. Razona la respuesta. b) (0,75 puntos) Considera ahora que el compilador desenrolla el bucle 2 veces y lo planifica para que se ejecute lo más rápido posible. ¿Cuantos ciclos tarda ahora en ejecutarse las dos primeras iteraciones del bucle (mas el código de inicialización)? Razona la respuesta. c) (0,5 puntos) Considera ahora un procesador VLIW con el mismo hardware anterior (incluidos los fallos de cache). La instrucción VLIW puede contener hasta 7 operaciones (una por cada una de las UFs disponibles). ¿En cuantos ciclos se ejecutan las dos primeras iteraciones del bucle (mas el código de inicialización)? Considera que el bucle se ha desenrollado 2 veces y se ha planificado convenientemente. ¿Cual es la densidad de código obtenida así como los MFLOPs que podríamos obtener para dicho código suponiendo un ciclo de reloj de 1 Ghz? Razona las respuestas. Solución 3 a) En primer lugar, como el cálculo del salto se realiza en la etapa ID consideramos un hueco de retardo para el salto de 1 ciclo. Tenemos en cuenta que los dos primeros accesos a memoria fallan en la cache L1, siendo resueltos en tres ciclos por la cache L2. En segundo lugar, comentar que aunque nos dice que el hardware es el mismo que en el problema anterior, en este caso vamos a considerar que existe cortocircuito entre las UFs en coma flotante, pues es lo habitual en los procesadores superescalares planificados estáticamente. Obtenemos la siguiente temporización (nótese como en cada ciclo emitimos como mucho 2 instrucciones): Ciclos 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 LD/ST lw r3, n LD/ST ALU addi r2, r0, r0 lf f0, x(r2) slli r4, r3, #2 ALU PF (DIV) Saltos lf f1, y(r2) divf f2, f0, f1 sf f2, z(r2) addui r2, r2, #4 sub r5, r4, r2 bnez r5, inicio lf f1, y(r2) lf f0, x(r2) divf f2, f0, f1 sf f2, z(r2) addui r2, r2, #4 sub r5, r4, r2 bnez r5, inicio Podemos ver que la etapa de ejecución tarda 29 ciclos en realizarse (teniendo en cuenta el hueco de retardo del salto). Añadiéndole las etapas de búsqueda, decodificación y escritura, obtenemos un tiempo total de 32 ciclos en ejecutar este código. Comparando con los 20 ciclos obtenidos en el caso del superescalar planificado dinámicamente, vemos que es bastante peor. Esto se debe a que los riesgos de datos frenan la emisión de instrucciones (debido a que no hay estaciones de reserva), por lo que se aprovecha poco que las unidades funcionales estén segmentadas. En la tabla anterior también podemos ver las detenciones producidas por el fallo de cache de las dos primeras operaciones de memoria. b) En este caso vamos a procurar mejorar el rendimiento del procesador superescalar planificado dinámicamente por medio de desenrollar el bucle dos veces y planificar adecuadamente el orden de las instrucciones para que se ejecute lo más rápido posible. Obtenemos la siguiente temporización: Ciclos 1 2 3 4 5 6 7 8 9 10 11 12 13 LD/ST lw r3, n LD/ST ALU addi r2, r0, r0 ALU PF (DIV) Saltos lf f0, x(r2) lf f1, y(r2) lf f01, x(r2+4) slli r4, r3, #2 lf f11, y(r2+4) divf f2, f0, f1 addui r2, r2, #8 sub r5, r4, r2 divf f21, f01,f11 sf f2, z(r2-8) bnez r5, inicio sf f21, z(r2-4) 4 Obsérvese como la 3a carga lf f1, y(r2) empieza en el ciclo 4 y la 4a carga lf f01, x(r2+4) en el ciclo 5, gracias a que las UFs de carga/almacenamiento están segmentadas. Ahora la mejora es espectacular. El tiempo total de ejecución es de 13 ciclos más las etapas de búsqueda, decodificación y escritura, lo que hace un total de 16 ciclos. Vemos como incluso mejora al planificado dinámicamente, lo que nos da una idea de lo interesante que es la planificación de código por parte del compilador y lo útil que es eliminar del código instrucciones de algún modo redundantes (addui y sub). c) En este caso obtenemos el siguiente diagrama: Ciclos 1 2 3 4 5 6 7 8 9 10 11 LD/ST lw r3, n NOP NOP lf f1, y(r2) lf f11, y(r2+4) NOP NOP NOP sf f2, z(r2-8) NOP sf f21, z(r2-4) LD/ST lf f0, x(r2) NOP NOP NOP lf f01, x(r2+4) NOP NOP NOP NOP NOP NOP ALU addi r2, r0, r0 NOP NOP NOP slli r4, r3, #2 NOP addui r2, r2, #8 sub r5, r4, r2 NOP NOP NOP ALU NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP PF (DIV) NOP NOP NOP NOP NOP divf f2, f0, f1 divf f21, f01,f11 NOP NOP NOP NOP PF (ADD) NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP Saltos NOP NOP NOP NOP NOP NOP NOP NOP NOP bnez r5, inicio NOP Como podemos ver, gracias a que el procesador VLIW puede lanzar más instrucciones en paralelo (en el ciclo 1 y en el ciclo 5 lanza 3 instrucciones), obtenemos un tiempo algo menor. El código por tanto se ejecuta en 14 ciclos (los 11 mostrados más 3 para las etapas IF, ID y WB). La densidad de código útil obtenida es: Densidad = 14 / (11 * 7) = 18,18 %. Para el cálculo de los MFLOPs, podemos ver que cada desenrollado del bucle (2 iteraciones y por tanto 2 operaciones en coma flotante) se ejecutará en 14 ciclos. Tenemos por tanto: MFLOPs = 2 ops / 14 ciclos @ 1 Ghz = 142,85 MFLOPs. ¤ PARTE ARQUITECTURAS MULTIPROCESADOR 3. (1,5 puntos) Explicar brevemente cada una de las cuestiones que a continuación se plantean (se valorará la capacidad de concreción del alumno). a) (0,25 puntos) Dada un multiprocesador formado por varios nodos, cada uno con la estructura siguiente, explicar a qué categoría o categorías de las estudiadas en clase puede pertenecer la máquina. b) (0,25 puntos) ¿En qué consiste un directorio jerárquico? ¿En qué se diferencia de un directorio plano basado en memoria? 5 c) (0,50 puntos) Para el siguiente fragmento de código explicar si es preferible (se producen menos fallos de cache) un protocolo de coherencia basado en actualización (DRAGON) o en invalidación (MESI). Suponer que inicialmente todas las variables están a cero, que flag y A están en líneas de memoria distintas y que las caches de los procesadores sólo contienen las variables locales de cada proceso. P1 P2 int i; int i; for (i=0; i<1000; i++) { while (flag) {} A = g(i); flag = 1; } for (i=0; i<1000; i++) { while (!flag) {} print A; flag = 0; } d) (0,25 puntos) ¿Qué es el ancho de banda de la bisección de una red? ¿Por qué es importante esta medida? e) (0,25 puntos) Explicar el problema más importante que la implementación de los cerrojos basada en array (array-based lock ) tiene en arquitecturas ncc-NUMA y cómo podría ser resuelto. Solución a) En primer lugar se trata de un multiprocesador de memoria compartida, puesto que la comunicación está integrada a nivel del sistema de memoria. Además, es un multiprocesador NUMA puesto que cada nodo tiene una memoria local “cercana”. Así pues, las categorías a las que podría pertenecer son cc-NUMA y ncc-NUMA. b) En un directorio jerárquico la información de directorio se organiza en una estructura de datos lógica de tipo árbol, en la que las hojas representan los nodos de procesamiento, cada uno con su memoria local, y los nodos intermedios contienen la información de directorio. La diferencia con un directorio plano basado en memoria es que para éste último la información de directorio para cada bloque de memoria reside en un lugar fijo y conocido (el nodo home), mientras que para un directorio jerárquico hay que encontrarla atravesando la jerarquía. c) El protocolo basado en actualización se comporta mejor. Hay dos variables compartidas: flag y A. La variable A es escrita por P1 y leída por P2, por lo que un protocolo de actualización generaría una transacción de Update en cada iteración del bucle (fallo de escritura), mientras que uno de invalidación generaría 2 (Fallo de escritura + fallo de lectura). Para la variable flag ocurre algo similar, se escribe y se lee en cada iteración del bucle (por procesadores diferentes), con lo que un protocolo basado en actualización genera menos fallos. d) Se define como la suma de los anchos de banda del mínimo número de enlaces que separan la red en dos mitades. Da una idea del ancho de banda máximo de una red de interconexión, ya que bajo tráfico uniforme la mitad de los mensajes habrán de “cruzar” la bisección. e) El problema es que la dirección de memoria sobre la que un proceso ha de realizar la espera activa no es conocida a priori en esta implementación de los locks. De esta forma, esta dirección podría corresponder con una memoria remota, con lo que no podría ser almacenada en la cache del proceso que realiza la espera activa. Esto ocasionaría una gran cantidad de tráfico (una transacción de red por cada lectura hecha durante la espera activa). La solución pasa por modificar la implementación de los cerrojos para asegurar que cada procesador realiza la espera activa sobre una posición de memoria local. Esto se hace, por ejemplo, en la implementación de los cerrojos con encolado software (Software Queuing Lock ). 6 ¤ 4. (3 puntos) Suponer un multiprocesador SMP con 10 procesadores y el protocolo de coherencia de caches MSI estudiado en clase. Para implementar las barreras se ha optado por la versión software centralizada con “cambio de sentido” (sense-reversal ) estudiada en clase. a) (0,5 puntos) Explica las variables compartidas necesarias para llevar a cabo esta implementación, así como el cometido de cada una de ellas. Para cada cada variable, indica también por cuáles de los procesos es accedida y cómo (por ejemplo, leída) b) (1 punto) Dado que el multiprocesador proporciona la versión de los cerrojos ticket-lock estudiada en clase, calcula el número de fallos de cache que se producirían cuando 10 procesos (cada uno ejecutándose en un procesador diferente) atravesaran una barrera. Para dicho cálculo suponer que los procesos alcanzan la llamada BARRIER exactamente a la misma vez y que el árbitro de bus otorga el control del mismo siguiendo un orden FIFO (para las peticiones que llegan al mismo tiempo el árbitro da prioridad a las de los procesadores con un identificador más bajo), que inicialmente la cache asociada a cada procesador no almacena ninguna variable compartida (sí las variables locales al proceso), y que las variables compartidas identificadas en el apartado a) residen en bloques de memoria diferentes. c) (0,5 puntos) Cambiamos el protocolo de coherencia MSI por uno MESI optimizado para el patrón migratorio (implementa la transición M → I para BusRead y el bloque es cargado en estado E en la cache del procesador que tiene el fallo de lectura1 ). Bajo las mismas condiciones del apartado anterior, ¿cuántos fallos de cache se producirían ahora? d) (0,5 puntos) El ISA de los procesadores de la máquina dispone del par de instrucciones LL y SC, que podrían ser empleadas directamente en el código de las barreras para ahorrar las llamadas a los procedimientos de librería LOCK y UNLOCK. Muestra cómo quedaría el código de la implementación de las barreras en este caso. e) (0,5 puntos) Para el nuevo código de las barreras, ¿cuántos fallos de cache se producirían? Solución a) La implementación software de las barreras centralizadas con “sense-reversal” emplea tres variables compartidas: counter : variable de tipo entero que se va incrementando con cada proceso que llega a la barrera. Cuando su valor alcanza el número de procesos se procede a la liberación de la barrera. Esta variable es leída y escrita por todos los procesos. lock : variable de tipo cerrojo que es empleada para asegurar el acceso en exclusión mutua a la variable anterior. Este cerrojo es adquirido y liberado por todos los procesos. flag: variable de tipo entero usada para realizar la espera ocupada. Todos los procesos la leen salvo el último en llegar a la barrera, que modifica su valor para notificar la liberación de la misma al resto. b) Para calcular el número de fallos de cache que se producen para que todos los procesos logren pasar la barrera, vamos a mostrar lo que ocurriría para el primer proceso que entra en la misma: 1 Notar que esto obliga a notificar a memoria los reemplazos de bloques de datos en estado E 7 El número de fallos de cache que se producen hasta que el primer proceso logra adquirir el cerrojo son: 10 como consecuencia de la instrucción fetch&increment (uno por proceso) + 10 como consecuencia de la lectura de now_serving (uno por proceso). Después, dentro de la sección crítica el proceso leerá y posteriormente escribirá el contenido del contador compartido, lo que ocasionará 2 fallos de cache con el protocolo MSI de clase. Finalmente, el lock es liberado mediante una escritura de now_serving que ocasiona un fallo de cache y el proceso que efectúa la liberación queda realizando la espera activa sobre el flag compartido, lo que ocasiona un nuevo fallo de cache. En total se producen 24 fallos de cache. El resto de procesos irán entrando en la sección crítica conforme vayan viendo que el valor del contador now_serving coincide con el valor de su ticket. De esa forma el número de fallos como consecuencia de la lectura de now_serving se irá reduciendo en 1 conforme un nuevo proceso entre en la P sección crítica y el número de fallos de cache para el resto de procesos salvo el último será de 7i=0 (13 − i). El último proceso en entrar a la barrera procede de la misma forma, salvo que en lugar de realizar la espera activa sobre el flag, escribe esta variable para notificar la liberación de la barrera, así como resetea el contador. Esto ocasiona 1 fallo de cache para escribir la variable flag. Para resetar el contador, sin embargo, no se produce fallo de cache, puesto que el proceso ya lo modificó con anterioridad y está en estado M en su cache. Finalmente, los procesos que están en la espera ocupada sufren 1 fallo de lectura cada uno para “ver” el nuevo valor de flag. De esta forma, el número total de fallos de cache es de: 24 + 13 + 12 + 11 + 10 + 9 + 8 + 7 + 6 + 5 + 9 = 114. c) La sustitución del protocolo MSI de clase por el MESI con la optimización del patrón migratorio descrita en el enunciado supone las siguientes modificaciones en cuanto a comportamiento se refiere: La primera lectura a un bloque de datos hará que se cargue en cache en estado E, y por lo tanto no ocasionará fallo en caso de que el procesador quiera escribirlo con posterioridad. Cuando un procesador sufre un fallo de lectura para un bloque que otro procesador tiene en estado M, el bloque de datos se trae en estado E (en lugar de S ), con lo que una posterior escritura por parte del que tiene el fallo de lectura no genera fallo de cache. De esta forma, nos ahorramos uno de los dos fallos que se producirían en la sección crítica como consecuencia de la lectura y posterior modificación de la variable counter, dado que la lectura traería el bloque de datos en estado E, con lo que la escritura posterior no ocasionaría fallo. De esta forma, el número total de fallos de cache es de: 23 + 12 + 11 + 10 + 9 + 8 + 7 + 6 + 5 + 4 + 9 = 104. d) El pseudocódigo de la implementación de las barreras con cambio de sentido utilizando el par LL/SC quedaría como sigue: struct bar_type { int counter; int flag = 0;} bar_name; BARRIER (bar_name, p) { local_sense = !(local_sense); incr: ll reg1, bar_name.counter addi reg1, reg1, 1 sc bar_name.counter, reg1 beqz incr 8 if (reg1 == p-) { bar_name.counter = 0; bar_name.flag = local_sense; } else while (bar_name.flag != local_sense) {}; } e) Asumiendo el protocolo MSI, el número de fallos de cache para esta nueva implementación se ve reducido de manera significativa. Cuando el primer proceso atraviesa la barrera se producen 10 fallos para las instrucciones de LL, 1 fallo para el SC que tiene éxito. Después todos los procesos salvo éste vuelven a intentar el LL. El proceso que ha logrado aumentar el valor de counter observaría un nuevo fallo de cache cuando intentase leer la variable flag usada para la implementación de la espera activa. En total 12 fallos de cache. P De esta forma, el número total de fallos sería: 9i=0 (12 − i) + 9 = 84 ¤ 5. (1,5 puntos) Para el siguiente segmento de código indica, justificando la respuesta, cuáles son los resultados posibles que se imprimirían por pantalla en los siguientes casos (supón que todas las variables están inicialmente a cero): P1 A=1 flag1 = 1 P2 while (!flag1) {} B=2 flag2 = 1 P3 while (!flag2) {} if (!flag1) print (“Error”) else print (A,B) a) (0,5 puntos) Multiprocesador que implementa el modelo de consistencia secuencial. b) (0,5 puntos) Multiprocesador que implementa un modelo de consistencia tipo PSO y preserva la atomicidad de las escrituras. c) (0,5 puntos) Multiprocesador que implementa el modelo de consistencia tipo PSO con escrituras no atómicas. Solución a) Bajo el modelo de consistencia secuencial el único resultado que podría dar la ejecución del programa es la impresión por pantalla del par (A,B) = (1,2). Puesto que la escritura de flag1 y flag2 se produce después de actualizar los valores de A y B, y además, la escritura de flag2 se produce después de la de flag1, éste es el único resultado posible. b) El modelo de consistencia PSO permite que una operación de lectura se complete antes que una escritura que la precede en el orden de programa, y que escritura a distintas posiciones de memoria puedan finalizar en desorden. Por otro lado el que las escrituras sean atómicas significa que son visibles a todos los procesos a la misma vez (una vez un proceso observa el valor de una escritura ésta es vista también por el resto). Para nuestro código, la atomicidad de las escrituras asegura que no se va a imprimir el mensaje de error por pantalla, ya que el hecho de que P2 vea la escritura a flag1 por parte de P1 significa que P3 también la va a ver tras observar el cambio del valor de flag2. Sin embargo, puesto que el orden de las escrituras que cada proceso hace no está garantizado de ninguna forma, el programa podría imprimir por pantalla uno de los siguiente 4 valores para el par (A,B) = {(0,0), (1,0), (0,2), (1,2)}. 9 c) Finalmente, si eliminamos la restricción de que las escrituras sean atómicas, además de los valores para (A,B) mostrados en el apartado anterior, la ejecución del código podría resultar en la impresión del mensaje de error por pantalla, ya que P3 podría observar la modificación de flag2 por parte de P2 pero no la de flag1 por parte de P1. ¤ 10