En la historia del hardware informático , algunas de las primeras unidades centrales de procesamiento de ordenador con conjunto de instrucciones reducido (CPU RISC) utilizaban una solución arquitectónica muy similar, ahora denominada canalización RISC clásica . Esas CPU eran: MIPS , SPARC , Motorola 88000 y, más tarde, la CPU DLX, inventada para la educación.
Cada uno de estos diseños RISC escalares clásicos busca e intenta ejecutar una instrucción por ciclo . El concepto común principal de cada diseño es una secuencia de instrucciones de ejecución de cinco etapas . Durante el funcionamiento, cada etapa de la secuencia trabaja en una instrucción a la vez. Cada una de estas etapas consta de un conjunto de flip-flops para mantener el estado y una lógica combinacional que opera en las salidas de esos flip-flops.
Las instrucciones residen en una memoria que tarda un ciclo en leerse. Esta memoria puede estar dedicada a la SRAM o a una caché de instrucciones . El término "latencia" se utiliza a menudo en informática y significa el tiempo que transcurre desde que se inicia una operación hasta que se completa. Por lo tanto, la obtención de instrucciones tiene una latencia de un ciclo de reloj (si se utiliza una SRAM de un solo ciclo o si la instrucción estaba en la caché). Por lo tanto, durante la etapa de obtención de instrucciones , se obtiene una instrucción de 32 bits de la memoria de instrucciones.
El contador de programa , o PC, es un registro que contiene la dirección que se presenta a la memoria de instrucciones. La dirección se presenta a la memoria de instrucciones al comienzo de un ciclo. Luego, durante el ciclo, la instrucción se lee de la memoria de instrucciones y, al mismo tiempo, se realiza un cálculo para determinar el próximo PC. El próximo PC se calcula incrementando el PC en 4 y eligiendo si se toma ese como el próximo PC o se toma el resultado de un cálculo de bifurcación/salto como el próximo PC. Tenga en cuenta que en RISC clásico, todas las instrucciones tienen la misma longitud. (Esto es algo que separa a RISC de CISC [1] ). En los diseños RISC originales, el tamaño de una instrucción es de 4 bytes, por lo que siempre agregue 4 a la dirección de la instrucción, pero no use PC + 4 para el caso de una bifurcación, un salto o una excepción (consulte bifurcaciones retrasadas , a continuación). (Tenga en cuenta que algunas máquinas modernas usan algoritmos más complicados ( predicción de bifurcación y predicción de objetivo de bifurcación ) para adivinar la siguiente dirección de instrucción).
Otra cosa que separa las primeras máquinas RISC de las máquinas CISC anteriores es que RISC no tiene microcódigo . [2] En el caso de las instrucciones microcodificadas CISC, una vez extraídas de la caché de instrucciones, los bits de instrucción se desplazan hacia abajo en la tubería, donde la lógica combinacional simple en cada etapa de la tubería produce señales de control para la ruta de datos directamente desde los bits de instrucción. En esos diseños CISC, se realiza muy poca decodificación en la etapa tradicionalmente llamada etapa de decodificación. Una consecuencia de esta falta de decodificación es que se deben utilizar más bits de instrucción para especificar lo que hace la instrucción. Eso deja menos bits para cosas como los índices de registro.
Todas las instrucciones MIPS, SPARC y DLX tienen como máximo dos entradas de registro. Durante la etapa de decodificación, los índices de estos dos registros se identifican dentro de la instrucción y se presentan a la memoria de registros como la dirección. De esta manera, los dos registros nombrados se leen desde el archivo de registros . En el diseño MIPS, el archivo de registros tenía 32 entradas.
Al mismo tiempo que se lee el archivo de registros, la lógica de emisión de instrucciones en esta etapa determina si el pipeline está listo para ejecutar la instrucción en esta etapa. Si no lo está, la lógica de emisión hace que tanto la etapa de obtención de instrucciones como la etapa de decodificación se detengan. En un ciclo de detención, los flip flops de entrada no aceptan nuevos bits, por lo que no se realizan nuevos cálculos durante ese ciclo.
Si la instrucción decodificada es una bifurcación o un salto, la dirección de destino de la bifurcación o del salto se calcula en paralelo con la lectura del archivo de registros. La condición de la bifurcación se calcula en el ciclo siguiente (después de leer el archivo de registros) y, si se realiza la bifurcación o si la instrucción es un salto, se asigna al PC de la primera etapa el destino de la bifurcación, en lugar del PC incrementado que se ha calculado. Algunas arquitecturas hicieron uso de la unidad lógica aritmética (ALU) en la etapa de ejecución, a costa de una ligera disminución del rendimiento de las instrucciones.
La etapa de decodificación terminó con una gran cantidad de hardware: MIPS tiene la posibilidad de ramificarse si dos registros son iguales, por lo que un árbol AND de 32 bits de ancho se ejecuta en serie después de la lectura del archivo de registros, lo que crea una ruta crítica muy larga a través de esta etapa (lo que significa menos ciclos por segundo). Además, el cálculo del objetivo de la ramificación generalmente requería una adición de 16 bits y un incrementador de 14 bits. La resolución de la ramificación en la etapa de decodificación hizo posible tener solo una penalización por predicción errónea de la ramificación de un solo ciclo. Dado que las ramificaciones se tomaban muy a menudo (y, por lo tanto, se predecían incorrectamente), era muy importante mantener baja esta penalización.
La etapa de ejecución es donde se lleva a cabo el cálculo real. Normalmente, esta etapa consta de una ALU y también de un desplazador de bits. También puede incluir un multiplicador y divisor de varios ciclos.
La ALU es responsable de realizar operaciones booleanas (and, or, not, nand, nor, xor, xnor) y también de realizar sumas y restas de números enteros. Además del resultado, la ALU normalmente proporciona bits de estado, como si el resultado fue 0 o no, o si se produjo un desbordamiento.
El cambiador de bits es responsable del cambio y las rotaciones.
Las instrucciones en estas máquinas RISC simples se pueden dividir en tres clases de latencia según el tipo de operación:
Si es necesario acceder a la memoria de datos, se hace en esta etapa.
Durante esta etapa, las instrucciones de latencia de un solo ciclo simplemente envían sus resultados a la siguiente etapa. Este reenvío garantiza que tanto las instrucciones de uno como de dos ciclos siempre escriban sus resultados en la misma etapa del proceso, de modo que solo se pueda usar un puerto de escritura en el archivo de registros y esté siempre disponible.
Para el almacenamiento en caché de datos mapeados directamente y etiquetados virtualmente, la más simple de las numerosas organizaciones de almacenamiento en caché de datos , se utilizan dos SRAM , una que almacena datos y la otra que almacena etiquetas.
Durante esta etapa, tanto las instrucciones de un solo ciclo como las de dos ciclos escriben sus resultados en el archivo de registros. Tenga en cuenta que dos etapas diferentes acceden al archivo de registros al mismo tiempo: la etapa de decodificación lee dos registros de origen, al mismo tiempo que la etapa de reescritura escribe el registro de destino de una instrucción anterior. En silicio real, esto puede ser un peligro (consulte a continuación para obtener más información sobre los peligros). Esto se debe a que uno de los registros de origen que se leen en la decodificación puede ser el mismo que el registro de destino que se escribe en la reescritura. Cuando eso sucede, las mismas celdas de memoria en el archivo de registros se leen y escriben al mismo tiempo. En silicio, muchas implementaciones de celdas de memoria no funcionarán correctamente cuando se lean y escriban al mismo tiempo.
Hennessy y Patterson acuñaron el término “riesgo” para referirse a situaciones en las que las instrucciones en una cadena de montaje producirían respuestas erróneas.
Los riesgos estructurales ocurren cuando dos instrucciones pueden intentar usar los mismos recursos al mismo tiempo. Las tuberías RISC clásicas evitaban estos riesgos replicando el hardware. En particular, las instrucciones de bifurcación podrían haber usado la ALU para calcular la dirección de destino de la bifurcación. Si la ALU se hubiera usado en la etapa de decodificación para ese propósito, una instrucción ALU seguida de una bifurcación habría hecho que ambas instrucciones intentaran usar la ALU simultáneamente. Es simple resolver este conflicto diseñando un sumador de destino de bifurcación especializado en la etapa de decodificación.
Los riesgos de datos ocurren cuando una instrucción, programada a ciegas, intenta utilizar datos antes de que estos estén disponibles en el archivo de registro.
En la clásica cadena de montaje RISC, los riesgos para los datos se evitan de una de dos maneras:
La omisión también se conoce como reenvío de operandos .
Supongamos que la CPU está ejecutando el siguiente fragmento de código:
SUB r3 , r4 -> r10 ; Escribe r3 - r4 en r10 Y r10 , r3 -> r11 ; Escribe r10 y r3 en r11
Las etapas de búsqueda y decodificación de instrucciones envían la segunda instrucción un ciclo después de la primera. Se transmiten por el conducto como se muestra en este diagrama:
En una cadena de montaje ingenua , sin consideración de riesgos, el riesgo de los datos progresa de la siguiente manera:
En el ciclo 3, la SUB
instrucción calcula el nuevo valor para r10
. En el mismo ciclo, AND
se decodifica la operación y r10
se obtiene el valor de del archivo de registros. Sin embargo, la SUB
instrucción aún no ha escrito su resultado en r10
. La reescritura de esto normalmente ocurre en el ciclo 5 (recuadro verde). Por lo tanto, el valor leído del archivo de registros y pasado a la ALU (en la etapa de ejecución de la AND
operación, recuadro rojo) es incorrecto.
En lugar de ello, debemos pasar los datos que se calcularon de SUB
nuevo a la etapa de ejecución (es decir, al círculo rojo del diagrama) de la AND
operación antes de que se escriban de nuevo normalmente. La solución a este problema es un par de multiplexores de derivación. Estos multiplexores se ubican al final de la etapa de decodificación y sus salidas en flop son las entradas a la ALU. Cada multiplexor selecciona entre:
AND
para detener la operación hasta que los datos estén listos.La lógica de la etapa de decodificación compara los registros escritos por las instrucciones en las etapas de ejecución y acceso de la canalización con los registros leídos por la instrucción en la etapa de decodificación, y hace que los multiplexores seleccionen los datos más recientes. Estos multiplexores de derivación permiten que la canalización ejecute instrucciones simples con solo la latencia de la ALU, el multiplexor y un flip-flop. Sin los multiplexores, la latencia de escritura y posterior lectura del archivo de registros tendría que incluirse en la latencia de estas instrucciones.
Tenga en cuenta que los datos solo se pueden pasar hacia adelante en el tiempo; no se pueden volver a una etapa anterior si aún no se han procesado. En el caso anterior, los datos se pasan hacia adelante (cuando el AND
está listo para el registro en la ALU, el SUB
ya lo ha calculado).
Sin embargo, tenga en cuenta las siguientes instrucciones:
LD adr -> r10 Y r10 , r3 -> r11
Los datos leídos de la dirección adr
no están presentes en la caché de datos hasta después de la etapa de acceso a la memoria de la LD
instrucción. En ese momento, la AND
instrucción ya ha pasado por la ALU. Para resolver esto, sería necesario que los datos de la memoria se pasaran hacia atrás en el tiempo hasta la entrada a la ALU. Esto no es posible. La solución es retrasar la AND
instrucción un ciclo. El peligro de los datos se detecta en la etapa de decodificación y las etapas de búsqueda y decodificación se detienen : se les impide cambiar sus entradas y, por lo tanto, permanecen en el mismo estado durante un ciclo. Las etapas de ejecución, acceso y reescritura posteriores ven una instrucción de no operación (NOP) adicional insertada entre las instrucciones LD
y AND
.
Este NOP se denomina burbuja de tubería , ya que flota en la tubería, como una burbuja de aire en una tubería de agua, ocupando recursos pero sin producir resultados útiles. El hardware para detectar un peligro de datos y detener la tubería hasta que se elimine el peligro se denomina interbloqueo de tubería .
Sin embargo, no es necesario utilizar un enclavamiento de canalización con ningún reenvío de datos. El primer ejemplo de SUB
seguido por AND
y el segundo ejemplo de LD
seguido por AND
se pueden resolver deteniendo la primera etapa durante tres ciclos hasta que se logre la reescritura y los datos en el archivo de registro sean correctos, lo que hace que la AND
etapa de decodificación de obtenga el valor de registro correcto. Esto causa un gran impacto en el rendimiento, ya que el procesador pasa mucho tiempo sin procesar nada, pero las velocidades de reloj se pueden aumentar ya que hay menos lógica de reenvío que esperar.
Este riesgo de datos se puede detectar con bastante facilidad cuando el compilador escribe el código de máquina del programa. La máquina Stanford MIPS dependía del compilador para agregar las instrucciones NOP en este caso, en lugar de tener los circuitos para detectar y (lo que es más complicado) bloquear las dos primeras etapas de la cadena de bloques. De ahí el nombre MIPS: Microprocessor without Interlocked Pipeline Stages (Microprocesador sin etapas de cadena de bloques interconectadas). Resultó que las instrucciones NOP adicionales agregadas por el compilador expandieron los binarios del programa lo suficiente como para que se redujera la tasa de aciertos de la caché de instrucciones. El hardware de bloqueo, aunque caro, se volvió a incluir en diseños posteriores para mejorar la tasa de aciertos de la caché de instrucciones, momento en el que el acrónimo ya no tenía sentido.
Los riesgos de control son causados por ramificaciones condicionales e incondicionales. La secuencia de comandos RISC clásica resuelve las ramificaciones en la etapa de decodificación, lo que significa que la recurrencia de resolución de la ramificación tiene una duración de dos ciclos. Hay tres implicaciones:
Hay cuatro esquemas para resolver este problema de rendimiento con ramas:
Las ramificaciones retrasadas fueron controvertidas, en primer lugar, porque su semántica es complicada. Una ramificación retrasada especifica que el salto a una nueva ubicación ocurre después de la siguiente instrucción. Esa siguiente instrucción es la que inevitablemente carga la caché de instrucciones después de la ramificación.
Las ramas retrasadas han sido criticadas [¿ por quién? ] como una mala elección a corto plazo en el diseño de ISA:
Supongamos que un RISC de 32 bits procesa una instrucción ADD que suma dos números grandes y el resultado no cabe en 32 bits.
La solución más simple, que ofrecen la mayoría de las arquitecturas, es la aritmética envolvente. A los números mayores que el valor codificado máximo posible se les quitan los bits más significativos hasta que quepan. En el sistema de números enteros habitual, 3000000000+30000000000=6000000000. Con la aritmética envolvente de 32 bits sin signo, 3000000000+30000000000=1705032704 (6000000000 mod 2^32). Esto puede no parecer demasiado útil. El mayor beneficio de la aritmética envolvente es que cada operación tiene un resultado bien definido.
Pero el programador, especialmente si programa en un lenguaje que admita números enteros grandes (por ejemplo, Lisp o Scheme ), puede no querer encapsular la aritmética. Algunas arquitecturas (por ejemplo, MIPS) definen operaciones de adición especiales que se ramifican a ubicaciones especiales en caso de desbordamiento, en lugar de encapsular el resultado. El software en la ubicación de destino es responsable de solucionar el problema. Esta ramificación especial se denomina excepción. Las excepciones se diferencian de las ramificaciones regulares en que la dirección de destino no está especificada por la propia instrucción, y la decisión de ramificación depende del resultado de la instrucción.
El tipo más común de excepción visible por software en una de las máquinas RISC clásicas es una falla de TLB .
Las excepciones son diferentes de las ramificaciones y saltos, porque esos otros cambios en el flujo de control se resuelven en la etapa de decodificación. Las excepciones se resuelven en la etapa de reescritura. Cuando se detecta una excepción, las siguientes instrucciones (anteriores en la tubería) se marcan como no válidas y, a medida que fluyen hacia el final de la tubería, sus resultados se descartan. El contador del programa se establece en la dirección de un controlador de excepciones especial y se escriben registros especiales con la ubicación y la causa de la excepción.
Para que sea fácil (y rápido) para el software solucionar el problema y reiniciar el programa, la CPU debe tomar una excepción precisa. Una excepción precisa significa que se han ejecutado todas las instrucciones hasta la instrucción de excepción, y que la instrucción de excepción y todo lo que sigue no se ha ejecutado.
Para tomar excepciones precisas, la CPU debe confirmar los cambios en el estado visible del software en el orden del programa. Esta confirmación en orden ocurre de manera muy natural en la secuencia clásica de RISC. La mayoría de las instrucciones escriben sus resultados en el archivo de registros en la etapa de reescritura, por lo que esas escrituras ocurren automáticamente en el orden del programa. Sin embargo, las instrucciones de almacenamiento escriben sus resultados en la cola de datos de almacenamiento en la etapa de acceso. Si la instrucción de almacenamiento toma una excepción, la entrada de la cola de datos de almacenamiento se invalida para que no se escriba en la memoria SRAM de datos de caché más tarde.
Ocasionalmente, la caché de datos o de instrucciones no contiene un dato o una instrucción requeridos. En estos casos, la CPU debe suspender la operación hasta que la caché se pueda llenar con los datos necesarios y luego debe reanudar la ejecución. El problema de llenar la caché con los datos requeridos (y potencialmente volver a escribir en la memoria la línea de caché expulsada) no es específico de la organización de la canalización y no se analiza aquí.
Existen dos estrategias para manejar el problema de suspensión/reanudación. La primera es una señal de bloqueo global. Esta señal, cuando se activa, impide que las instrucciones avancen por el pipeline, generalmente desactivando el reloj de los flip-flops al comienzo de cada etapa. La desventaja de esta estrategia es que hay una gran cantidad de flip-flops, por lo que la señal de bloqueo global tarda mucho tiempo en propagarse. Dado que la máquina generalmente tiene que bloquearse en el mismo ciclo en el que identifica la condición que requiere el bloqueo, la señal de bloqueo se convierte en una ruta crítica que limita la velocidad.
Otra estrategia para manejar la suspensión/reanudación es reutilizar la lógica de excepción. La máquina toma una excepción en la instrucción infractora y todas las demás instrucciones se invalidan. Cuando la caché se ha llenado con los datos necesarios, la instrucción que causó la falla de caché se reinicia. Para acelerar el manejo de la falla de caché de datos, la instrucción se puede reiniciar de modo que su ciclo de acceso ocurra un ciclo después de que se llene la caché de datos.