En este capítulo vamos a ver cómo mejorar las prestaciones de la CPU mediante los procesadores segmentados (o en pipeline), los cuales incorporan una técnica para acelerar el ritmo de ejecución de las instrucciones. En primer lugar empezaremos por comentar algunos aspectos básicos para introducir los conceptos en los que se apoya esta técnica. A continuación nos centraremos en nuestro procesador de referencia, el MIPS64, viendo la configuración de las etapas que componen su cauce. Como veremos, hay diversos factores que pueden impedir un aprovechamiento óptimo del concepto de pipeline en la CPU. Trataremos estos factores y las técnicas para evitarlos en la medida de lo posible. Por último abordaremos una mejora más del pipeline mediante el concepto de operaciones multiciclo que reducirán las esperas o paradas generadas por las instrucciones pesadas, como las multiplicaciones, divisiones o las operaciones en coma flotante. Arquitectura de Computadores Segmentación (Pipeline) - 1 La primera opción que se nos ocurre para aumentar la velocidad de un procesador es aumentar la velocidad del reloj, lo cual es muy fácil, pero claro, si los circuitos que componen las etapas de ejecución del procesador no soportan esa velocidad, éste se quema o deja de funcionar correctamente. Así, para poder aumentar la velocidad del reloj, primero se deben hacer más rápidos los circuitos con los que se construyen los procesadores y la memoria principal. No obstante, se debe considerar el coste que supone una mejora, y que el límite de esta velocidad lo impone el estado del arte actual de la tecnología. Otra posibilidad es organizar el hardware para poder ejecutar más de una instrucción simultáneamente: concurrencia. La concurrencia se puede obtener en dos niveles: al nivel del procesador y al nivel de la instrucción. La concurrencia al nivel de la CPU se obtiene disponiendo de múltiples procesadores ejecutando simultáneamente varias instrucciones. Obtener concurrencia a nivel de la instrucción significa poder ejecutar varias instrucciones simultáneamente con una única CPU. Este último tipo de paralelismo se denomina segmentación, aunque suele ser más conocido por su denominación en inglés: pipelining. Las arquitecturas con múltiples procesadores suelen utilizarse en máquinas de muy altas prestaciones (y muy alto precio). Sin embargo, con arquitecturas segmentadas se consigue una muy buena mejora del rendimiento y a un coste asequible. Por esto, es normal que todos los microprocesadores actuales de propósito general incorporen el pipelining. Ya que es muy común su utilización en los actuales procesadores, vamos a abordar aquí esta técnica del pipelining, mientras que las arquitecturas multiprocesador las dejaremos para asignaturas o textos de arquitecturas paralelas o avanzadas. Arquitectura de Computadores Segmentación (Pipeline) - 2 Ahora ya podemos abordar el concepto de pipeline. El proceso en pipeline (o segmentado) es similar al utilizado en cualquier cadena de montaje, y el nombre pipeline (tubería) se debe al hecho de que, como en una tubería, en la entrada se aceptan nuevos elementos (instrucciones) antes de que los previamente aceptados salgan por la salida. Empecemos con el ejemplo de una cadena de montaje. Supongamos una gran pastelería en la que las tartas primero se hacen en el horno y después se empaquetan para la venta. El proceso de empaquetar una tarta consiste en: 1. Poner una caja vacía en la mesa. 2. Meter una tarta en la caja. 3. Cerrar y precintar la caja. 4. Poner una etiqueta en la caja. 5. Llevar la caja a un gran contenedor. Si cada una de estas operaciones la realiza un operario en 10 segundos, parece claro que se tarda 50 s en empaquetar una tarta y, por lo tanto, en empaquetar 10 tartas se tardaría 500 s. Arquitectura de Computadores Segmentación (Pipeline) - 3 Ahora supongamos que se dispone de una cadena de empaquetado de tartas con una cinta transportadora sobre la que trabajan cinco operarios especializados en tareas distintas. El primer operario pone la caja-1 en la cinta transportadora, y ésta avanza hasta que la caja-1 está donde el segundo operario, que introduce una tarta dentro de la caja-1, al mismo tiempo que el primer operario pone otra caja-2 en la cinta. La caja-1 sigue avanzando hasta el tercer operario, que la cierra y la precinta, al mismo tiempo que el segundo operario mete otra tarta en la caja-2 y el primer operario pone otra caja-3 en la cinta. La caja-1 sigue su camino en la cinta pasando por el cuarto operario, que pone una etiqueta, hasta llegar al quinto operario, que la retira de la cinta. En el momento que el quinto operario retira la caja de la cinta, hay cuatro cajas más en la cinta. Si cada una de estas fases de empaquetado se realiza en 10 s, a partir de ahora, cada 10 s saldrá una nueva tarta empaquetada, en lugar de hacerlo cada 50 s que se tardaba cuando no había cadena de empaquetado. A partir de ahora, solamente se tardará100 segundos en tener 10 tartas empaquetadas, mientras que en el caso de cuando se tenía un solo operario se tardaba 500 segundos. Debe quedar claro que aunque ahora sale una nueva tarta empaquetada cada 10 s, la preparación completa de cada tarta sigue requiriendo 50 s (igual que cuando había una sola persona preparando las tartas). Ha aumentado el rendimiento, pero se mantiene el tiempo de empaquetado de cada tarta. Si calculamos el rendimiento de los dos sistemas de empaquetado de tartas, veremos que el rendimiento en este último caso se ha multiplicado por 5 (¡igual a número de etapas!). Arquitectura de Computadores Segmentación (Pipeline) - 4 Según lo que acabamos de ver, parece que interesa dividir las fases de ejecución de las instrucciones en más etapas, para así obtener un mayor rendimiento en la ejecución. La ejecución de una instrucción podría descomponerse en las siguientes 5 etapas: 1. 2. 3. 4. 5. F: D: E: M: W: Alimentación de la instrucción (fetch) Decodificación de la instrucción / Lectura de registros Ejecución (en la ALU) / Cálculo de la dirección efectiva Acceso a memoria Escritura del resultado en registros de la CPU Si ahora la ejecución de una instrucción está descompuesta en 5 etapas, cada etapa puede durar aproximadamente 1/5 de la duración total de la ejecución de la instrucción. Si suponemos que la duración de un ciclo de reloj es igual a la duración de cada una de estas pequeñas etapas, podemos decir, en principio, que con la técnica de la segmentación (o pipelining) se consigue que a cada ciclo de reloj finalice una instrucción, o lo que es lo mismo, una velocidad de instrucción por ciclo. Debemos tener en cuenta que: • Cada etapa dispone de los recursos hardware necesarios para realizar su cometido. • Las ejecuciones de las instrucciones se solapan. • Todas las etapas tienen la misma duración (ciclo de reloj). • La duración del ciclo de reloj lo fija la etapa más lenta. Arquitectura de Computadores MIPS64 - 5 En este histograma podemos ver la comparación entre dos procesadores de similares características y que su gran diferencia consiste en estar o no estar segmentados. Utilizaremos el procesador del ejemplo que hemos visto en la página anterior, en el que supondremos que el ciclo de reloj es 1 ns. El ciclo de reloj es igual para ambos casos (1 ns), ya que partimos de dos procesadores con similares características. El tiempo de ejecución de la instrucción (si todas las instrucciones pasan por todas las etapas) también es igual en ambos casos (5 ciclos). En cuanto al rendimiento, el procesador no segmentado ejecuta una instrucción, que tarda 5 ciclos, y hasta que no termina, no comienza la ejecución de otra instrucción, por lo que termina una instrucción cada 5 ciclos, o sea, que se consigue un rendimiento de 1 instrucción cada 5 ciclos. En cambio, en el caso del procesador segmentado, cuando una instrucción arranca, al ciclo siguiente se arranca la siguiente instrucción y, de esta manera, cuando la primera instrucción termina, al ciclo siguiente termina la segunda. De esta manera, tenemos que a cada ciclo finaliza una instrucción, consiguiente un rendimiento de una instrucción por ciclo o, como vemos en el gráfico, de 5 instrucciones cada 5 ciclos. Arquitectura de Computadores Segmentación del Cauce - 6 Como habíamos visto anteriormente, la aceleración o speed up, es el cociente entre el tiempo medio que transcurre entre la finalización de las instrucciones en el procesador no segmentado y en el segmentado. En el procesador segmentado, en condiciones ideales, a cada ciclo finaliza la ejecución de una instrucción, consiguiendo un número medio de ciclos por instrucción (CPI) igual a 1. No debemos olvidar que en un procesador segmentado (una vez que está lleno el cauce y en condiciones ideales), en un momento dado hay tantas instrucciones en ejecución como etapas tiene; no obstante, cada etapa se está ocupando de una función distinta de cada instrucción. Arquitectura de Computadores Segmentación del Cauce - 7 Veamos la mejora de aceleración que se experimenta al segmentar un procesador convencional, cuando ejecuta un programa que consta del 40% de instrucciones aritméticas, un 20% de instrucciones de salto y otro 40% de instrucciones de acceso a operandos en memoria principal. Las instrucciones aritméticas del procesador no segmentado requieren 4 ciclos, mientras que las de salto y acceso a memoria consumen 5 ciclos de reloj. El procesador segmentado consta de 5 etapas, y tiene un retardo de 0,2 ns para el control y el paso de datos a cada etapa posterior. El ciclo de reloj en ambos casos es de 1 ns. El tiempo medio de ejecución de una instrucción en el procesador NO segmentado se obtiene mediante la suma de los tiempos de ejecución de cada tipo de instrucción por sus frecuencias de aparición en los programas, obteniendo así, un valor medio de 4,6 ns. En el caso del procesador segmentado, tenemos que cada etapa requiere 1 ciclo de reloj (1 ns). Así tenemos que cada instrucción tardará en ejecutarse un total de 5 ns, pero, en realidad, cada 1 ns finalizará una instrucción. De esta manera, la mejora en rendimiento (speed up) que se experimenta es el cociente entre el tiempo medio que transcurre entre la finalización de las instrucciones en el procesador no segmentado y el segmentado, consiguiendo así, una mejora del 4,6. Esto significa que, para programas con esta distribución de frecuencias en sus instrucciones, el procesador segmentado es 4,6 veces más rápido que el convencional. Arquitectura de Computadores MIPS64 - 8 Arquitectura de Computadores MIPS64 - 9 Una vez vistos los conceptos básicos del pipeline, vamos a concretarlo en una versión simplificada de la arquitectura MIPS64. En un principio vamos a considerar una versión básica de 5 etapas, en la que la Unidad Aritmético-Lógica solamente va a operar con datos enteros. El tiempo de ejecución de cada etapa va a ser un ciclo. En esta versión básica (solo para un grupo reducido de instrucciones) vamos a disponer de una estructura en la que veremos que las instrucciones de salto generan ciertos retardos. Más adelante mejoraremos la estructura para conseguir mejorar el problemas de los retardos. Ahora iremos mostrando en las siguientes páginas la descripción de cada una de las 5 etapas de este pipeline simplificado de MIPS64. Arquitectura de Computadores MIPS64 - 10 En esta primera etapa se extrae o alimenta una instrucción de memoria, de la dirección indicada en el registro Contador de Programa (PC), y se almacena en el registro de instrucción IR (Instruction Register). A continuación se calcula la dirección de la siguiente instrucción en secuencia (por dirección en memoria), añadiéndole al PC el tamaño de la instrucción alimentada. En nuestro caso, todas las instrucciones son de longitud constante, 4 bytes. La dirección calculada se guarda en el registro NPC (Next Program Counter). La instrucción queda en el registro IR de donde se irá extrayendo información en las sucesivas etapas. Arquitectura de Computadores Segmentación del Cauce - 11 Aquí se realizan 3 funciones: • Decodificación de la instrucción alimentada, es decir, se averigua cuál es la operación que se debe realizar. • En paralelo se lee el contenido de los registros indicados como operandos en la instrucción, y se guardan en los registros temporales A y B. • Si la instrucción contiene un campo de 16 bits con un valor inmediato, se le hace una extensión de signo a 64 bits y se guarda en el registro Inm. Es posible que, según la operación a realizar, tanto el contenido de los registros leídos como el valor inmediato extraído, tenga o no sentido. Si la operación no requiere alguno de los registros leídos o el valor inmediato, simplemente no se utiliza. En cualquier caso, no se pierde tiempo al hacerlo, ya que la lectura se realiza, en paralelo, al mismo tiempo que la decodificación. Arquitectura de Computadores MIPS64 - 12 Esta es la etapa de la Unidad Aritmético-Lógica (ALU), donde se realiza una de estas cuatro posibles funciones, dependiendo del tipo de instrucción: • Instrucción con registros (A op B à ALUoutput). La ALU realiza la operación indicada por el código de función u operación, utilizando los valores de los registros temporales A y B. El resultado se deja en el registro temporal ALUoutput. • Instrucción registro-Inmediato (A op Inm à ALUoutput). La ALU realiza la operación indicada por el código de función u operación, utilizando los valores del registro temporal A y el valor del registro Inm (que contiene el valor inmediato de la instrucción). El resultado se deja en el registro temporal ALUoutput. • Referencia a memoria (A+Inm à ALUoutput). La ALU suma estos operandos para formar una dirección absoluta de memoria. El resultado se deja en el registro ALUoutput. • Bifurcación. En este tipo de instrucciones, la ALU hace dos cosas: Primero añade al NPC el valor inmediato Inm desplazado 2 bits a la izquierda (o sea, multiplicado por 4) para crear un desplazamiento y calcular la dirección de destino el salto, dejando el resultado en ALUoutput. A continuación se comprueba si el registro A (cuyo valor se ha establecido en la etapa D) tiene el valor 0, y el resultado de la comparación se establece en el registro Cond. Estas cuatro funciones distintas pueden estar en la misma etapa (aunque solo se ejecuta una de ellas en cada instrucción) ya que nunca se requiere que se haga más de una de estas operaciones sobre la misma instrucción. Los desplazamientos de los saltos se indican como múltiplos de 4 pues como todas las instrucciones ocupan 4 bytes, siempre están en una dirección múltiplo de 4. Arquitectura de Computadores MIPS64 - 13 Para todo tipo de instrucciones, en esta etapa se actualiza el PC con el valor del NPC. Además, dependiendo del tipo de instrucción, se pueden realizar dos operaciones: • Referencia a memoria. Dependiendo de si se trata de una carga o un almacenamiento, se realiza una acción. Si es una carga, el dato traído de memoria se deja en el registro temporal LMD; si se trata de un almacenamiento, el dato del registro temporal B se escribe en memoria. En cualquier caso, la dirección de memoria utilizada es la calculada en el ciclo (etapa) anterior y que se dejó en ALUoutput. • Bifurcación. Si la instrucción es una bifurcación y se cumple la condición del salto, el valor que se lleva al PC es la dirección almacenada en ALUoutput, en lugar del valor contenido en el NPC. Arquitectura de Computadores MIPS64 - 14 En esta etapa se escribe un valor en alguno de los registros generales (si la instrucción lo requiere), bien si el valor viene de una posición de memoria (valor en registro temporal LMD) o se trata del resultado de una operación en la ALU. El registro en el que se deja el resultado de la operación con la ALU, depende del tipo concreto de instrucción ejecutada (tipo R o tipo I). Arquitectura de Computadores MIPS64 - 15 A modo de resumen, aquí tenemos un esquema general de las etapas de MIPS64 1. F (Alimentación de instrucción – Instruction Fetch). Se extrae una instrucción de la dirección de memoria indicada por el Contador de Programa (PC) y se incrementa éste en 4, para que apunte a la dirección de la siguiente instrucción. 2. D (Decodificación de Instrucción / Lectura de registros). Se decodifica la instrucción extraída y se lee el contenido de los registros indicados como operandos en la instrucción. Si es una instrucción de salto, se calcula la dirección de destino incrementando el PC con el desplazamiento indicado en la instrucción. En cualquier caso, la decodificación se realiza en paralelo con la lectura de los registros. 3. E (Ejecución/Cálculo de la dirección de la dirección efectiva). Aquí, dependiendo del tipo de instrucción, se realiza una de estas funciones: • Cálculo de dirección efectiva (registro base + desplazamiento), si es un salto o carga/ almacenamiento. • Ejecución de una operación en la ALU (Unidad Aritmético-Lógica), si es una instrucción aritmética, lógica, desplazamiento… 4. M (Acceso a memoria). Si la instrucción es de acceso a memoria, para lectura o escritura (carga/almacenamiento), se realiza en esta fase, utilizando para ello el valor del registro leído en la fase D (si es una escritura en memoria) y la dirección de memoria calculada en la fase E. 5. W (Escritura en registros). Si el resultado de la instrucción, o lectura de memoria, tiene como destino un registro, se escribe en éste en esta fase. Arquitectura de Computadores Segmentación del Cauce - 16 En esta figura se muestra una primera aproximación para conseguir la arquitectura de MIPS que se ha comentado en las páginas anteriores, y se puede ver cómo fluye una instrucción a través del cauce. No obstante, esta aproximación provisional no se corresponde con el cometido de las etapas visto en la página anterior, cuya arquitectura exacta la abordaremos más adelante realizando las mejoras oportunas. Éste es un pipeline simplificado para la ejecución de estas operaciones: − Load/Store − Operaciones con enteros − Salto condicional si “igual a cero” Como puede verse, al final de cada etapa, los valores obtenidos en ella, deben guardarse para poder acceder a ellos desde una etapa posterior. Por ello, se salvan en unos registros temporales (IR, A, B, Inm, NPC, ALUoutput, LMD, Cond). Se puede apreciar que algunos registros están ubicados en las etapas en las que se lee su contenido, pero su escritura o actualización se produce en otras etapas. Así, el registro PC está situado en la etapa de extracción de la instrucción (F), pero, en realidad, se escribe desde la etapa M. Los registros generales están ubicados en la etapa D, pero se escriben durante la etapa WB. Estas señales de flujo hacia atrás le generan mucha complejidad al cauce, ya que, como pronto veremos, son los responsables de los “riesgos” o peligros del pipeline. Arquitectura de Computadores Segmentación del Cauce - 17 En la figura de arriba se muestran algunas de las características del pipeline básico de la arquitectura MIPS64. Arquitectura de Computadores Segmentación del Cauce - 18 Arquitectura de Computadores MIPS64 - 19 Una vez elegido el número óptimo de etapas, para que el factor de aceleración sea igual al número de etapas se requiere que todas las etapas del pipeline siempre estén llenas de instrucciones útiles, y que nada retrase el avance de las instrucciones a través del pipeline. Por desgracia, no es fácil mantener siempre ocupadas todas las etapas del pipeline. Hay tres causas que lo impiden: • Motivos estructurales. • Dependencias de operandos. • Instrucciones de bifurcación. En las siguientes transparencias las comentaremos con cierto detalle. Arquitectura de Computadores Segmentación (Pipeline) - 20 Como ya veremos, se tiende a que la ejecución de cada etapa se realice en un ciclo de reloj. Pues bien, cuando una etapa no es capaz de realizar su cometido en un ciclo de reloj, el pipeline se detiene hasta que dicha etapa finaliza su trabajo. Hay varias causas estructurales (arquitectura del pipeline) que pueden hacer que el pipeline se detenga. Por ejemplo, puede ocurrir que no todas las etapas sean de la misma duración, con lo que alguna etapa de corta duración debería esperar a que acabe la siguiente que es más larga. Esto hará que la duración efectiva de cada etapa sea igual a la duración de la etapa más larga. Normalmente los procesadores actuales tienden a un alto número de etapas, con lo que automáticamente tienden a igualarse los tiempos. Otra cosa que también puede ocurrir es que desde varias etapas se quiera acceder a memoria simultáneamente (por ejemplo en la etapa de alimentación de instrucción y en la escritura del resultado). Y, claro, si una etapa se detiene para esperar a poder realizar el acceso a memoria, el pipeline se para. También tenemos que considerar que no todas las instrucciones hacen las mismas cosas, por lo que requieren tiempos distintos de CPU. Pasemos a la siguiente página para tratar este caso con más detalle. Arquitectura de Computadores Segmentación (Pipeline) - 21 No todas las instrucciones hacen las mismas cosas y requieren el mismo tiempo de CPU. Unas pueden necesitar más tiempo en la etapa de ejecución (por ejemplo, la carga o escritura de un registro requiere menos trabajo de ALU que una división en coma flotante), mientras que otras pueden necesitar más tiempo para obtener los operandos o escribir el resultado (si están en memoria principal se tarda más que si están en registros). En el ejemplo de la transparencia vemos que la instrucción I2 no puede completar la fase de ejecución en el ciclo 4, necesitando para ello también los ciclos 5 y 6. Esto hace que en el 5º ciclo no pueda alimentarse la instrucción I5 por estar ocupada la etapa de extracción de instrucción, debiendo esperar ésta al ciclo 7 para poder continuar extrayendo instrucciones. Obsérvese que como consecuencia del sobretiempo de E2, al término de los ciclos 6 y 7 no finaliza ninguna instrucción (lo cual va en perjuicio del rendimiento). Puede suceder incluso que alguna de las etapas ni siquiera necesite ejecutarse. Por ejemplo, en un procesador cuya última etapa se dedique a escribir en memoria principal, la carga de un registro no requerirá dicha última etapa. En este caso, simplemente sucederá que cuando una instrucción corta va después de una normal, ambas finalicen su ejecución simultáneamente, y en el siguiente ciclo de reloj no terminará ninguna instrucción; por lo tanto, el rendimiento no varía. Arquitectura de Computadores Segmentación (Pipeline) - 22 Si desde dos etapas se accede a un mismo recurso, una de las etapas tendrá que detener su ejecución y esperar a que quede libre el recurso necesario. Por ejemplo, en las etapas de Fectch y Ejecución puede ser necesario un acceso simultáneo a la ALU para realizar una operación aritmética, como el incremento que debe hacer al PC para actualizarlo (en F) o la suma que se requiere para realizar el cálculo de la dirección efectiva de un operando (en E). Esto se soluciona fácilmente mediante dos sumadores distintos. Por otra parte, también se puede requerir un acceso simultáneo a memoria desde las etapas F y M; para extraer la siguiente instrucción a ejecutar y para leer o escribir un operando o resultado en memoria. Arquitectura de Computadores Segmentación del Cauce - 23 Para mejorar el acceso simultáneo a algunos recursos, como los registros, el ciclo se divide en dos subciclos, de tal manera que dos etapas pueden coincidir en el recurso en el mismo ciclo. Por ejemplo, la etapa W podría escribir en un registro en el primer subciclo, y otra instrucción podría leer su contenido en el segundo subciclo, desde la etapa D (lectura de registros). Arquitectura de Computadores Segmentación del Cauce - 24 Las dependencias de datos se producen cuando dos instrucciones comparten un dato (operando o resultado). Hay tres tipos de dependencias de datos: • Verdadera dependencia de datos à RAW (Read After Write) • Antidependencia • Dependencia de salida à WAR (Write After Read) à WAW (Write After Write) Los distintos tipos de dependencias pueden dan lugar a otros tantos tipos de “riesgos de datos” (RAW, WAR y WAW). Realmente, la única dependencia verdadera de datos es la que da lugar a riesgo de tipo RAW, que viene dada por la propia lógica del programa; los otros dos tipos son “dependencias de nombre” y dependen de las características del cauce. Más adelante trataremos las dependencias de nombre, pero ahora vamos a ocuparnos de la verdadera dependencia de datos (RAW). La situación es la siguiente: Una instrucción Ij actualiza el valor de una variable, pero una instrucción posterior, Ik, realiza una lectura de esa variable antes de que Ij haya terminado la operación escribiendo la variable compartida. Veamos, en las siguientes páginas ejemplos concretos de esta dependencia de datos y varias soluciones propuestas. Arquitectura de Computadores Segmentación (Pipeline) - 25 En el programa del ejemplo, la dependencia que se denomina “lectura después de escritura” (Read After Write, o RAW) puede producirse entre las instrucciones I2 e I3 si la instrucción dmul lee el contenido de R1 (en el segundo subciclo de t4) antes de que el resultado de la suma anterior (en el primer subciclo de t6) se cargue en él. Obviamente, la operación dmul no se ejecutará con los operandos esperados por el programador, por lo que el resultado del programa será incorrecto. Hay dos opciones básicas para resolver este problema de dependencia de datos; uno es mediante la prevención: evitando que pueda llegarse a esta situación de dependencia; el otro es mediante la detección y resolución, es decir, no preocupándose de evitarlo, pero sí de detectarlo en caso de que se produzca y solucionarlo de alguna manera. Veámoslas en detalle. Arquitectura de Computadores Segmentación (Pipeline) - 26 La dependencia de datos: Prevención. El problema de la dependencia de datos entre una instrucción I1 y otra instrucción I2 que le sigue puede prevenirse retrasando la ejecución de I2 un número K de etapas hasta que desaparezca el problema de que I2 lea un operando que I1 no ha escrito todavía. Este retraso puede conseguirse insertando un número K de instrucciones entre I1 e I2. Esto significa que el compilador tiene que reordenar el programa para encontrar K instrucciones que puedan ejecutarse después de I1 y antes de I2 sin que por ello varíe la estructura lógica del programa. Ejemplo 1: En la figura tenemos el ejemplo de un programa en el que hay una dependencia entre las instrucciones I2 e I3 a causa del registro R1. Como vemos, en este programa el compilador puede detectar la dependencia de datos y reorganizar las instrucciones para retardar el acceso al registro R1 hasta que esté actualizado. Debe quedar claro que esta reorganización solamente puede hacerse si se mantiene la semántica original del programa. Por lo que hemos visto en el ejemplo de la página anterior, para evitar la dependencia de I3 respecto a I2, se requiere que I3 comience su ejecución tres ciclos después de que lo haga I2. (Suponemos que en el primer subciclo de t6, se escribe el resultado de daddi en R1, y en el segundo subciclo, se lee el operando R1 de dmul). Como se puede apreciar, esto se ha conseguido con la reorganización que ha realizado el compilador, intercambiando el orden I2 por I1 e I3 por I4. Arquitectura de Computadores Segmentación (Pipeline) - 27 Si el compilador no puede reorganizar el código para encontrar estas K instrucciones que decíamos arriba, sin modificar la lógica del programa, debe insertar operaciones NOP (No Operación) entre las operaciones dependientes. Ejemplo 2: En el ejemplo inferior tenemos el fragmento de un programa en el que también hay dependencias entre las instrucciones I1, I2 e I3. En este caso vemos que las instrucciones de este fragmento no se pueden reordenar sin alterar la lógica del programa, por lo que el compilador inserta las instrucciones NOP necesarias para evitar la ejecución errónea de instrucciones por las dependencias de datos. La ventaja de la solución basada en la prevención es que no se requiere hardware adicional, pero a expensas de un compilador más complejo y una pérdida de tiempo si es necesario insertar instrucciones NOP (cuando no se puede reordenar el programa para insertar instrucciones útiles). Arquitectura de Computadores Segmentación (Pipeline) - 28 La dependencia de datos: Detección y resolución. Este método requiere un hardware adicional en la CPU, pues se deben detectar las dependencias de datos durante la ejecución y resolver estas dependencias. Detectarlas significa que debe darse cuenta de que en un momento dado hay dos instrucciones arrancadas I1 e I2, tal que I2 depende de un resultado establecido por I1. El dispositivo hardware que detecta estas dependencias se denomina interlock. Resolver las dependencias significa hacer algo para retrasar la ejecución de I2 o para acelerar, en la medida de lo posible, la entrega a I2 del resultado que produce I1. Veamos dos posibilidades de resolución (no excluyentes) para cuando se detecta una dependencia de datos entre dos etapas del pipeline. • Detener el pipeline. La aproximación más sencilla para evitar los problemas de dependencias de datos con ayuda del hardware es detener la actividad en las etapas necesarias del pipeline hasta que desaparezca la dependencia, es decir, hasta que se pueda ejecutar correctamente la instrucción dependiente. En el ejemplo de la figura, esto significa detener las instrucciones que siguen a la instrucción daddi desde el ciclo 4 hasta que el registro R1 pueda leerse debidamente actualizado en segundo subciclo de t6. Con esta estrategia, primero se detecta la dependencia y después se detiene el pipeline a partir de la instrucción dependiente hasta que desaparece la dependencia. Obsérvese que la detención de n ciclos del cauce tiene el mismo efecto que la inserción de n instrucciones NOP. Arquitectura de Computadores Segmentación (Pipeline) - 29 El otro modo de resolver las dependencias de datos mediante Detección y Resolución es acelerando, en la medida de lo posible, la entrega a una instrucción I2 del resultado que produce una instrucción previa I1. Veámoslo en detalle: Anticipación (data forwarding). En una dependencia de datos RAW, puede suceder que una instrucción I2 necesite un operando en la etapa D (decodificación y obtención de operandos) que debe producirlo en el mismo ciclo la instrucción I1 en su etapa E (ejecución). Esto obligaría a detener la instrucción I2 hasta que I1 escriba el resultado en su etapa W, y entonces pueda continuar I2 en su etapa D y leer el resultado que produjo I1. Este retraso puede evitarse redirigiendo (forwarding) el resultado de la etapa E de la instrucción I1 directamente a la entrada de la etapa E de la instrucción I2, obteniendo el mismo efecto que se obtendría en la ejecución de la etapa D de I2. En la figura se muestra el esquema de la ejecución de 3 instrucciones, donde la I2 tiene una dependencia de datos de I1 (por R1), e I3 la tiene a su vez de I2 (por R2). Para evitar la lectura incorrecta de datos o las paradas del cauce, se puede dirigir el resultado de la etapa E de I1 directamente a la entrada de la etapa E de I2. Igualmente, también se puede dirigir el resultado de la etapa E de I2 directamente a la entrada de la etapa E de I3. En este caso se consigue evitar totalmente la detención del pipeline, pero hay otras situaciones en las que esto no es posible. Arquitectura de Computadores Segmentación (Pipeline) - 30 Aquí tenemos otro caso de dependencias múltiples que se intentan resolver con anticipación, pero en la situación de la instrucción de carga I1, su resultado (R1), no se consigue hasta el final de la etapa M (la de acceso a memoria), por lo que hay que detener I2 en su etapa D para dar tiempo a que I1 ejecute su etapa M y, en el ciclo siguiente, la instrucción de suma pueda obtener el valor actualizado de R1 a la entrada de su etapa de ejecución . Como vemos, en las instrucciones de carga no se consigue evitar por completo la parada del cauce (se requiere una parada de un ciclo), pero si no hubiera adelantamiento, se necesitaría una parada de 2 ciclos adicionales ciclos en la etapa F de la suma, pues habría que esperar a que se completara la etapa W de la instrucción de carga para dar paso a la etapa D (obtención de operandos) de la suma. Arquitectura de Computadores Segmentación (Pipeline) - 31 Como se ha mostrado en las páginas anteriores, hay más de una secuencia de instrucciones que conduce a distintas situaciones de adelantamientos. Aquí mostramos algunas situaciones de adelantamientos en MIPS, en cuyo caso, los adelantamientos siempre se producen desde el final de la etapa de ejecución E (casos 1, 2 y 3) o desde el final de una lectura en memoria (casos 4, 5 y 6) para cargar un registro. El adelantamiento se produce hacia la etapa de la instrucción en la que se requiere el dato de entrada. En los casos 3 y 5, el adelantamiento se produce hacia la etapa D, ya que es la encargada de comprobar la condición de salto (considerando la arquitectura mejorada que veremos en breve). En el caso 5, lo cierto es que no se consigue ninguna ventaja con un adelantamiento, pues en el mismo ciclo en el que la etapa M de la instrucción de carga puede adelantarle R1 a la instrucción de bifurcación en su etapa D, se ejecuta la etapa W de la instrucción de carga, escribiendo R1 en el primer subciclo, y la instrucción de bifurcación puede leerlo en el segundo subciclo de su etapa D. Arquitectura de Computadores Segmentación del Cauce - 32 Ya hemos comentado que uno de los principales problemas en el diseño de un pipeline consiste en asegurar el mantenimiento de un flujo constante de instrucciones alimentando sus diversas etapas para así poder mantener también constante el ritmo de ejecución de instrucciones (idealmente, una por ciclo). El flujo normal de ejecución de un programa es secuencial, por lo que las instrucciones que se van alimentando y ejecutando están en direcciones consecutivas de memoria. Por desgracia, las instrucciones de bifurcación (que suelen representar alrededor del 20% de las instrucciones ejecutadas) pueden romper el flujo constante de instrucciones alimentadas. Cuando se alimenta una instrucción en la CPU, lo primero que se suele hacer en la etapa Fetch es incrementar el registro Contador de Programa (PC) para conocer la dirección de la siguiente instrucción a ejecutar y extraerla en el siguiente ciclo de reloj. Pero si se trata de una instrucción de salto condicional, hay que esperar a una etapa posterior para que se pueda saber si se cumple o no la condición de salto, por lo que la etapa de alimentación de instrucción no sabe de dónde seguir alimentando instrucciones. ¡Tenemos un problema con los saltos! Arquitectura de Computadores Segmentación (Pipeline) - 33 Una instrucción de bifurcación condicional hace que la dirección de la siguiente instrucción a ejecutar no se conozca en la etapa F, por lo que esta etapa de alimentación no puede extraer la siguiente instrucción a una bifurcación hasta que ésta llegue a la etapa en la que ya se conoce la dirección de la siguiente instrucción a ejecutar. (En la ruta de datos que hemos visto anteriormente, esto sucede en la etapa M). Por esto, a continuación de la alimentación de la instrucción de bifurcación, las etapas F, D… se van quedando vacías al no saber qué instrucción alimentar. A estas etapas vacías que aparecen se las denomina huecos de retardo (delay slots). En algunos sistemas, las bifurcaciones incondicionales pueden detectarse en la fase de alimentación o extracción (fetch) si se le añade un poco hardware a esta primera etapa. Este hardware de ayuda también extrae la dirección de salto, con lo que se puede proseguir la extracción de instrucciones de la dirección del salto. Sin embargo, en el caso de las bifurcaciones condicionales no se puede hacer esto, pues puede ocurrir que la condición del salto se establezca precisamente en la instrucción anterior, con lo que no hay más remedio que esperar a que la bifurcación llegue a la etapa de comprobación de la condición de salto y establezca la dirección de la siguiente instrucción a ejecutar. Esto quiere decir que se debe detener la alimentación de instrucciones al pipeline hasta que en la etapa que corresponda se averigüe la dirección de la siguiente instrucción a ejecutar. Afortunadamente, hay diversas técnicas que pueden evitar o minimizar el impacto de las instrucciones de bifurcación, tales como la “bifurcación retardada”, la “predicción del salto” y algunas más. Veamos estas dos primeras técnicas. Arquitectura de Computadores Segmentación del Cauce - 34 Una mejora del cauce de MIPS: Ahora la dirección del salto se calcula en la etapa D, por lo que, ante un salto, solamente se retarda un ciclo la alimentación de la siguiente instrucción (lo óptimo sería poder alimentarla en la etapa F). Obsérvese que cuando la instrucción de salto está en la etapa D, sería deseable que en la etapa F se pudiera alimentar ya la siguiente instrucción. Con este cauce, la siguiente instrucción se alimenta un ciclo después de la etapa D, por lo que solamente se pierde un ciclo en los saltos. Arquitectura de Computadores Segmentación del Cauce - 35 Con la mejora comentada, ahora la dirección de salto se establece en la etapa D, con lo que solamente hay que esperar un ciclo más (que en una instrucción distinta de salto) para conocer la dirección de la siguiente instrucción, por lo que solo se produce un hueco de retardo. Arquitectura de Computadores Segmentación del Cauce - 36 El problema de los saltos: Bifurcación retardada. Ya hemos visto que cuando entra una instrucción de salto en el pipeline, se producen h huecos de retardo por lo que hay que esperar h ciclos hasta que llega la siguiente instrucción de la secuencia a ejecutar. Estaría bien que estos huecos se pudieran rellenar con instrucciones que siempre se deban ejecutar, independientemente de si se toma la bifurcación o no. Pero claro, si estas instrucciones están en memoria inmediatamente después de la instrucción de bifurcación, según lo que sabemos hasta ahora, no se ejecutarán si la instrucción de bifurcación decide saltar. Para conseguir que se ejecuten siempre las h instrucciones que siguen a una bifurcación, en los procesadores que tienen bifurcaciones retardadas, las instrucciones de salto no tienen efecto hasta h instrucciones después de su ejecución, por lo que independientemente del resultado de la ejecución de la bifurcación, siempre se ejecutan las h instrucciones siguientes. De esta manera no se producen los huecos de retardo. Arquitectura de Computadores Segmentación (Pipeline) - 37 Supongamos la serie de instrucciones de la transparencia, en la que I6 es la instrucción de salto condicional a la instrucción I11. En una CPU sin saltos retardados, la secuencia de instrucciones ejecutadas cuando la bifurcación no tiene lugar es: I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, ... Mientras que cuando sí se produce la bifurcación, la secuencia es: I1, I2, I3, I4, I5, I6, I11, I12, ... Si a este mismo procesador (con un hueco de retardo en las bifurcaciones) se le ponen bifurcaciones retardadas, la secuencia de instrucciones ejecutadas cuando se produce la bifurcación es: I1, I2, I3, I4, I5, I6, I7, I11, I12, ... Es decir I7, se ejecuta siempre, haya o no bifurcación. Esto es así porque el efecto de la bifurcación se retarda un ciclo de reloj, es decir, después de alimentar una instrucción de salto, se siguen extrayendo y ejecutando las instrucciones siguientes, según su orden en la memoria, durante un ciclo más de reloj. Después, se establece en el contador de programa la dirección indicada en la instrucción de salto, con lo que la 2ª instrucción después de la de salto, será ya la correspondiente a la de la dirección indicada en dicha instrucción de salto. Arquitectura de Computadores Segmentación (Pipeline) - 38 Aquí tenemos un ejemplo, en un ensamblador hipotético, en el que para un programa dado (el de la izquierda), se muestran en la parte derecha las dos posibilidades de ejecución en un procesador con un hueco de retardo en los saltos: tanto cuando no se toma la bifurcación, como cuando sí se toma. Arquitectura de Computadores MIPS64 - 39 A partir de un fragmento de código escrito para un procesador sin saltos retardados, si se va a ejecutar en una CPU con saltos con un hueco de retardo, sabiendo lo que sucede en los procesadores con bifurcaciones retardadas (tarda un ciclo más en bifurcar realmente), vamos a intentar reordenar nuestro fragmento de código para que su ejecución se produzca con la misma semántica que la esperada en un procesador sin saltos retardados. Así, después de la instrucción de salto vamos a poner una de las instrucciones que hay antes de la de salto, es decir, una instrucción que queremos que se ejecute siempre (se produzca el salto o no). Así, suponiendo que no se altera la semántica del programa, podríamos mover la instrucción I4 y ponerla justo a continuación de I6 (la bifurcación condicional), quedando entonces la secuencia de instrucciones en memoria así: I1, I2, I3, I5, I6, I4, I7, I8, I9, I10, I11, I12, ... Ahora vamos a ejecutar este programa en nuestro procesador con bifurcaciones retardadas de un ciclo. Cuando no se produce la bifurcación, la secuencia de instrucciones que se ejecuta es: I1 – I2 – I3 – I5 – I6 – I4 – I7 – I8 – I9 – I10 – I11 – I12 –... Si la bifurcación tiene lugar, las instrucciones se ejecutarán en este orden: I1 – I2 – I3 – I5 – I6 – I4 –I11 – I12 –... Si no hubiese bifurcación retardada, al alimentar una instrucción de salto condicional habría que detener la alimentación de instrucciones hasta que la instrucción de salto pasase por la etapa de decodificación (hasta saber cuál es la siguiente instrucción a ejecutar). Con bifurcación retardada se aprovechan ese ciclo perdido en alimentar y ejecutar una instrucción que se desea ejecutar incondicionalmente antes de que la instrucción de bifurcación tenga lugar (se salte o no). Arquitectura de Computadores Segmentación (Pipeline) - 40 Esta técnica de los saltos retardados requiere la colaboración del compilador, que debe saber cómo reorganizar el código para rellenar los huecos de retardo con instrucciones útiles (de la misma manera que se hacía con las dependencias de datos). Si el compilador no encuentra una manera de reordenar el código sin afectar a su semántica, debe insertar tantas operaciones NOP con huecos de retardo como sean necesarias para las bifurcaciones de ese procesador. Arquitectura de Computadores Segmentación (Pipeline) - 41 El problema de los saltos: Predicción del salto. Otra técnica para reducir el problema de las bifurcaciones consiste en intentar predecir si una instrucción de bifurcación saltará o no. Por ejemplo, una bifurcación al final de un bucle salta al comienzo de éste todas las veces excepto la última. Según esto, sería ventajoso que cuando el procesador se encuentra una instrucción de salto suponga que el salto sí se va a efectuar realmente, y cuando la etapa de alimentación detecte una bifurcación, empiece a extraer instrucciones de la dirección de destino del salto. Si una vez que se ejecuta la instrucción de bifurcación, resulta que efectivamente se salta, la ejecución continúa normalmente, pues las instrucciones de la dirección del salto son las que ya se están alimentando. Si por el contrario resulta que no se realiza el salto, se debe vaciar el pipeline (desechar todas las instrucciones alimentadas) y empezar a alimentar las instrucciones que siguen en secuencia. Arquitectura de Computadores Segmentación (Pipeline) - 42 Si ahora suponemos que el control del bucle se realiza mediante una instrucción al comienzo del mismo, ahora lo normal será suponer que la bifurcación no se tomará hasta la última pasada del bucle. Es decir, hay veces que conviene suponer una cosa y otras veces otra. Esto que hemos visto se denomina ejecución especulativa, pues las instrucciones pueden empezar a ejecutarse antes de que el procesador sepa que las instrucciones alimentadas son las realmente correctas. Supongamos que se predice que el salto tendrá lugar, por lo que se empiezan a alimentar instrucciones y a pasarlas a las siguientes etapas del pipeline antes de que la instrucción de bifurcación finalice su etapa de ejecución. ¡Y si al ejecutar la bifurcación no se realiza el salto! ¡Nos encontramos que algunas instrucciones ya se han empezado a ejecutar en las etapas anteriores! Con ejecución especulativa se debe tener cuidado de que en las etapas anteriores a la de ejecución no se modifiquen registros o posiciones de memoria hasta que no se confirme que la predicción realizada ha sido la acertada. Arquitectura de Computadores Segmentación (Pipeline) - 43 La repetición de la bifurcación que se toma en el bucle la puede detectar el compilador y establecer la predicción mediante un cierto código de operación en la bifurcación, lo que quiere decir que, en ejecución, siempre que se alimente esa instrucción de bifurcación se hará la misma predicción. Por esto, se la conoce como predicción estática. La predicción se puede mejorar si se realiza dinámicamente, para lo cual el hardware del procesador debe establecer la posibilidad de que haya o no salto cada vez que se encuentre una cierta instrucción de bifurcación. Para ello, en la CPU se debe llevar la cuenta de los resultados de las últimas ejecuciones de cada bifurcación. Ciertos estudios estadísticos dicen que conservando solamente el resultado de la última ejecución (con un bit) ya se obtiene una gran probabilidad de acierto (del orden del 90%) y con la historia de las cuatro últimas ejecuciones (dos bits) se mejora ligeramente; manteniendo una historia mayor, la mejora es despreciable. Arquitectura de Computadores Segmentación (Pipeline) - 44 Como hemos visto, la predicción ante un salto puede ser “saltar” o “no saltar”, para decidir por dónde continúa extrayendo instrucciones. Para la arquitectura MIPS, con una porción de código concreta y con los mismos datos, vamos a ver lo que sucede cuando la predicción es “saltar” y, posteriormente, cuando es “no saltar”. Predicción NO SALTAR: Después de alimentar la instrucción de salto, se continúa con la Decodificación, pero hasta el final de esta etapa no se sabrá si habrá que saltar o no. Como la predicción es “no saltar”, en la etapa F se alimenta la siguiente instrucción en memoria. Al final de la etapa D se ve que se ha cometido un fallo en la predicción, por lo que habrá que eliminar la instrucción daddi del cauce y empezar a extraer y ejecutar la instrucción de destino del salto. Se ha perdido un ciclo de tiempo. Predicción SALTAR: Igual que antes, después de alimentar la instrucción de salto, se continúa con la Decodificación. Como la predicción es “saltar”, en este ciclo se querría extraer la siguiente instrucción de la dirección del salto, pero como hasta el final de la Decodificación tampoco se conoce la dirección del salto, en este ciclo no se puede extraer ninguna instrucción, y hay que esperar al final de la Decodificación para poder alimentar la instrucción de destino del salto. Como vemos, en MIPS, ante una condición que establezca que se debe saltar, se pierde un ciclo tanto con una predicción de “saltar” como de “no saltar”. Por el contrario, si la condición se evaluara como falsa (no saltar), con la predicción de NO SALTAR, sí se podría seguir alimentando instrucciones de la siguiente dirección de memoria. Por lo que hemos visto, en MIPS 64 no tiene ninguna utilidad predecir “Tomar el salto”, por lo que su predicción es NO SALTAR. Arquitectura de Computadores Segmentación del Cauce - 45 Las instrucciones de sumas y restas en aritmética entera pueden pasar por la etapa de ejecución realizando su cometido en un solo ciclo, pero hay otras operaciones, como la multiplicación, la división o, en general, las instrucciones con datos de coma flotante, que no pueden ejecutar la operación de la etapa E en un solo ciclo. En este capítulo vamos a ver posibles soluciones a esta cuestión y también los problemas que van a surgir. Arquitectura de Computadores MIPS64 - 46 La arquitectura de MIPS64 que hemos visto hasta ahora solamente contempla la aritmética con enteros, pero los procesadores deben soportar también operaciones en coma flotante. Las operaciones en coma flotante requieren bastante más tiempo en ejecutarse que las de la aritmética entera, pueden durar de un par de ciclos, como la negación; hasta más de un centenar, como la raíz cuadrada. Para adaptar esto a nuestro pipeline, una opción sería alargar el tiempo del ciclo de reloj hasta el necesario para ejecutar las instrucciones de coma flotante, pero esto no sería práctico, porque estaríamos ralentizando la ejecución del resto de las instrucciones, haciéndolas tan lentas como las de coma flotante. Otra posibilidad podría consistir en mantener la duración del tiempo de ciclo de los enteros y repetir la etapa de Ejecución tantas veces como fuera necesario como para ejecutar cada instrucción de coma flotante. Esto también tendría la pega de que la ejecución de una instrucción de coma flotante pararía el cauce hasta su terminación. Este último problema se puede solucionar si dotamos al cauce de múltiples unidades funcionales en la etapa E: una para enteros, otras para multiplicación, división, para aritmética en coma flotante, etc. Veámoslo en la siguiente página Arquitectura de Computadores Segmentación del Cauce - 47 Nuestro nuevo cauce podría tener: • Una unidad principal de enteros, cargas y almacenamientos, y gestión de bifurcaciones. • Multiplicador de enteros y coma flotante. • Sumas y restas y conversiones en coma flotante. • División de enteros y coma flotante. Disponiendo de varias unidades funcionales, si comienza a ejecutarse una operación de coma flotante, no impediría que pasara a ejecutarse una instrucción posterior de suma de enteros. Pero seguimos teniendo un problema. Si a una operación en coma flotante (que es de larga duración) le sigue otra más también en coma flotante, esta última tendrá que esperar a que termine completamente la primera para comenzar su ejecución. Esta es la situación que teníamos antes de inventar el pipeline. La solución a este problema es obvia: hay que segmentar las unidades de coma flotante en múltiples etapas, cada una de un ciclo de duración. Así, un ciclo después del comienzo de una instrucción en la unidad de coma flotante podría arrancar la siguiente instrucción, aunque también sea de coma flotante. Como vemos en el gráfico de nuestro ejemplo, las instrucciones que se ejecuten en la unidad de enteros, E, solamente requieren un ciclo; las sumas en coma flotantes se ejecutaran en 4 ciclos; y las multiplicaciones, en 7. La unidad funcional de las divisiones no está segmentada, y requiere 24 ciclos de reloj para ejecutarse. Ya que esta última unidad funcional no está segmentada, se pueden producir riesgos estructurales si hay dos divisiones consecutivas o cercanas en el tiempo. Arquitectura de Computadores Segmentación del Cauce - 48 Ya que las unidades funcionales multiciclo duran más, hay mayores riesgos, estructurales y de datos. Comencemos viendo los riesgos estructurales. El caso de la división es muy claro. Dadas dos instrucciones de división consecutivas, la segunda no puede arrancarse un ciclo después de la primera, pues su unidad funcional no está segmentada, por lo que antes de emitir la segunda división (pasarla a su etapa de ejecución), debe esperar en D a que finalice la etapa E de la división anterior. Arquitectura de Computadores Segmentación del Cauce - 49 Ya que algunas instrucciones duran más ciclos que otras, en un momento dado puede haber varias instrucciones en el cauce que lleguen a un punto en el que intenten ejecutar la misma etapa al mismo tiempo. Este es el caso del ejemplo de arriba, en el que tres instrucciones compiten por la etapas M y W. Obsérvese que aunque las 3 instrucciones llegan al mismo tiempo a la etapa M, solamente la instrucción de carga la utiliza realmente (las otras dos utilizan registros), por lo que el acceso a memoria no supone un riesgo estructural. En cambio, sí compiten realmente por utilizar W, la etapa de escritura de registros, pues todas ellas generan un resultado cuyo destino es un registro. Veamos cómo se soluciona este conflicto en la página siguiente. Arquitectura de Computadores Segmentación del Cauce - 50 Ante el problema estructural de la competencia por un recurso, se podría disponer de un conjunto de registros generales con más de un puerto de escritura, pero esto no suele hacerse, pues estas situaciones solamente se producen en muy raras ocasiones. Normalmente, la solución adoptada consiste en detectar la situación del riesgo estructural (competencia por el recurso), detener las instrucciones que compiten y dejarlas continuar en secuencia, de una en una. Lo razonable es dejar continuar la instrucción de mayor latencia (la que genera mayor retardo), pues mientras no avance, es la que tiene más probabilidades de causar más riesgos de datos a otras instrucciones. El resto de las instrucciones que compiten por entrar en la etapa W, pueden quedar detenidas en la etapa M, a la espera de que W quede libre. En el caso de MIPS, estas instrucciones quedan detenidas en la etapa D, que es donde se comprueban los riesgos estructurales. Arquitectura de Computadores Segmentación del Cauce - 51 Ya habíamos visto los problemas que se pueden producir debidos a dependencias entre operaciones cercanas en el tiempo. Ahora, estos problemas se pueden complicar con las operaciones multiciclo y, además, veremos que aparecen nuevos tipos de riesgos de datos. Las dependencias entre instrucciones pueden ser de datos y de nombre. Las dependencias de datos son las que existen entre dos instrucciones que comparten datos. Las dependencias de nombre se producen cuando dos instrucciones utilizan el mismo registro o dirección de memoria (comparten el nombre), pero realmente no hay flujo de datos entre tales instrucciones (no comparten el dato). Hay dos tipos de dependencias de nombre: antidependencias y dependencias de salida. Los distintos tipos de dependencias pueden dan lugar a otros tantos tipos de “riesgos de datos” (RAW, WAR y WAW). La verdadera dependencia de datos (RAW) viene dada por la propia lógica del programa, mientras que las otras dos pueden producirse dependiendo de las características del cauce del procesador. • Verdadera dependencia de datos àRAW (Read After Write) • Dependencia de salida à WAW (Write After Write) • Antidependencia à WAR (Write After Read) En los cauces con operaciones multiciclo, además de que las dependencias RAW se pueden producir más fácilmente, también aparecen los otros dos tipos de dependencias. Arquitectura de Computadores MIPS64 - 52 Los riesgos RAW de las dependencias entre instrucciones ya los teníamos en los cauces convencionales, pero ahora, con las operaciones multiciclo, se van a producir con mayor facilidad. En el ejemplo de arriba, las dos operaciones de suma y almacenamiento de enteros pueden ejecutarse sin ningún problema pese a que tienen la dependencia de R1, ya que, como ya sabemos, aunque la escritura y la lectura de R1 coinciden en el mismo ciclo, la suma puede escribir el resultado en R1 en el primer subciclo, y el almacenamiento puede leer R1 en el segundo subciclo. En el caso de que estas operaciones fueran con datos en coma flotante, las cosas cambian. La etapa de ejecución de la suma ahora requiere más tiempo, por lo que es multiciclo; así que dura 3 ciclos más que con los enteros. Ahora nos encontramos que cuando la operación de almacenamiento llega a la etapa D para leer el operando R1, la suma todavía no ha llegado a la etapa W de escritura del resultado en R1. Ante este escenario, la solución de MIPS es parar la instrucción de almacenamiento en la etapa D, hasta que el operando R1 quede actualizado en la etapa W de la suma. Arquitectura de Computadores Segmentación del Cauce - 53 En el pipeline que habíamos visto hasta hace poco, cuando solamente operábamos con enteros, varias instrucciones se ejecutaban simultáneamente en el cauce, e iban finalizando en el mismo orden en el que comenzaban. Con la incorporación de la coma flotante y la necesidad de incluir unidades funcionales multiciclo, nos encontramos con el escenario de la parte inferior de la figura, en el que ¡las instrucciones no finalizan en el mismo orden en el que se emiten! La multiplicación en coma flotante es una operación multiciclo muy larga, por lo que finaliza después de su sucesora, una suma, también en coma flotante, pero 2 ciclos más corta. La suma en coma flotante también es unos ciclos más larga que la carga y el almacenamiento que le siguen, por lo que, igualmente, también finaliza más tarde que éstas. Esto va a generar unas nuevas formas de dependencias de datos que veremos en las siguientes páginas. Arquitectura de Computadores MIPS64 - 54 La dependencia de salida (Write After Write) se produce cuando una instrucción i y una instrucción j escriben su resultado en el mismo registro o dirección de memoria. Se debe preservar el orden de ejecución entre las dos instrucciones para asegurar que el valor que se escribe finalmente es el que corresponde a la instrucción j. En el fragmento de programa de arriba, vemos que debido a la gran latencia de la multiplicación, ésta finaliza después de la suma, aún empezando ésta posteriormente. Así, el valor que al final queda escrito en el registro F1 es el que deja la multiplicación, lo cual es incorrecto. Obsérvese que este efecto solamente se produce cuando el resultado de la multiplicación se escribe (en F1) sin que ninguna instrucción lo utilice posteriormente como operando de entrada. Así, si entre la multiplicación y la suma hubiese una resta, por ejemplo, que tomase el resultado de la multiplicación como dato de entrada, ya no se produciría el problema pues al impedir el riesgo RAW entre la multiplicación y la resta, se evita también el riesgo WAW entre la multiplicación y la suma. Obsérvese que la suma no puede entrar en la etapa D hasta que la resta pasa a la etapa de ejecución. Arquitectura de Computadores Segmentación del Cauce - 55 En general, hay dos soluciones para los riesgos de datos de tipo WAW: 1. Desechar el resultado de la multiplicación, puesto que no se va a utilizar por ninguna instrucción. O lo que es más fácil, sustituir la instrucción de multiplicación por una NOP (no Operación). 2. Detener la emisión de la suma (la instrucción posterior), hasta que la multiplicación (la anterior) entre en la etapa de Memoria. De esta manera se asegura que la instrucción posterior no escribirá el registro de destino antes que la anterior instrucción. Esta es la solución de MIPS a los riesgos WAW. No olvidemos tampoco, que este tipo de problemas solo se pueden producir con programas en los que hay secuencias de instrucciones sin sentido, es decir, instrucciones que producen resultados que no son utilizados por ninguna instrucción posterior. Arquitectura de Computadores Segmentación del Cauce - 56 Los riesgos debidos a las antidependencias (WAR) entre dos instrucciones i y j se producen cuando la instrucción j escribe un registro o posición de memoria que la instrucción i utiliza como operando de entrada. En el ejemplo de arriba (en un procesador hipotético), si debido a una ejecución fuera de orden, la resta se ejecuta antes que la suma, el operando de entrada F8 de la suma, no sería el correcto, pues tomaría el valor producido como resultado de la resta, lo cual no era lo pretendido en el programa original. Estos problemas se producen en las arquitecturas con planificación dinámica, que conllevan ejecución fuera de orden. También podría producirse en pipelines en los que, por algún motivo, se produzcan lecturas en etapas finales y escrituras al comienzo del cauce. Arquitectura de Computadores Segmentación del Cauce - 57 Los riesgos WAR no se producen en nuestro procesador MIPS, ya que utiliza un cauce estático, es decir, que se respeta el orden de emisión o arranque de las instrucciones. Obsérvese que aunque la finalización de las instrucciones sí puede producirse fuera de orden, la emisión o arranque sí se realiza en orden. Además, los operandos se leen enseguida, en la etapa D, y las escrituras en registros se producen al final, en W. En este ejemplo, la instrucción de suma deja el resultado en F2, que lo utiliza la instrucción anterior como operando de entrada. Aunque la suma finaliza antes que la multiplicación, no afecta a la ejecución correcta, porque la multiplicación toma le valor de F2 en su etapa D, mucho antes de la etapa W de la suma. Arquitectura de Computadores Segmentación del Cauce - 58
© Copyright 2024 ExpyDoc