En la arquitectura informática , una ranura de retardo es una ranura de instrucción que se ejecuta sin los efectos de una instrucción anterior. [1] La forma más común es una única instrucción arbitraria ubicada inmediatamente después de una instrucción de bifurcación en una arquitectura RISC o DSP ; esta instrucción se ejecutará incluso si se toma la bifurcación anterior. Esto hace que la instrucción se ejecute fuera de orden en comparación con su ubicación en el código del lenguaje ensamblador original .
Los diseños de procesadores modernos generalmente no utilizan ranuras de retardo y, en su lugar, realizan formas cada vez más complejas de predicción de bifurcaciones . En estos sistemas, la CPU pasa inmediatamente a lo que cree que será el lado correcto de la bifurcación y, por lo tanto, elimina la necesidad de que el código especifique alguna instrucción no relacionada, que puede no ser siempre obvia en tiempo de compilación. Si la suposición es incorrecta y se debe llamar al otro lado de la bifurcación, esto puede introducir una demora prolongada. Esto ocurre con la suficiente poca frecuencia como para que la aceleración de evitar la ranura de retardo se compense fácilmente con la menor cantidad de decisiones incorrectas.
Una unidad central de procesamiento generalmente ejecuta instrucciones del código de la máquina mediante un proceso de cuatro pasos: primero se lee la instrucción de la memoria, luego se decodifica para comprender lo que se debe realizar, luego se ejecutan esas acciones y, finalmente, los resultados se vuelven a escribir en la memoria. En los primeros diseños, cada una de estas etapas se realizaba en serie, de modo que las instrucciones necesitaban un múltiplo del ciclo de reloj de la máquina para completarse. Por ejemplo, en el Zilog Z80 , el número mínimo de relojes necesarios para completar una instrucción era cuatro, pero podía llegar a ser de 23 relojes para algunas instrucciones (raras). [2]
En cualquier etapa del procesamiento de una instrucción, solo interviene una parte del chip. Por ejemplo, durante la etapa de ejecución, normalmente solo está activa la unidad aritmético lógica (ALU), mientras que otras unidades, como las que interactúan con la memoria principal o decodifican la instrucción, están inactivas. Una forma de mejorar el rendimiento general de una computadora es mediante el uso de una secuencia de instrucciones . Esto agrega algunos circuitos adicionales para mantener los estados intermedios de la instrucción a medida que fluye a través de las unidades. Si bien esto no mejora la sincronización del ciclo de ninguna instrucción individual, la idea es permitir que una segunda instrucción use las otras subunidades de la CPU cuando la instrucción anterior se haya movido. [3]
Por ejemplo, mientras una instrucción está utilizando la ALU, la siguiente instrucción del programa puede estar en el decodificador y una tercera puede ser extraída de la memoria. En este tipo de disposición de línea de ensamblaje , el número total de instrucciones procesadas en cualquier momento puede ser mejorado hasta por el número de etapas de pipeline. En el Z80, por ejemplo, un pipeline de cuatro etapas podría mejorar el rendimiento general en cuatro veces. Sin embargo, debido a la complejidad de la sincronización de las instrucciones, esto no sería fácil de implementar. La arquitectura de conjunto de instrucciones (ISA) mucho más simple del MOS 6502 permitió que se incluyera un pipeline de dos etapas, lo que le dio un rendimiento que era aproximadamente el doble del del Z80 a cualquier velocidad de reloj dada. [4]
Un problema importante con la implementación de pipelines en los primeros sistemas era que las instrucciones tenían recuentos de ciclos muy variables. Por ejemplo, la instrucción para sumar dos valores a menudo se ofrecía en múltiples versiones, u opcodes , que variaban en función de dónde leían los datos. Una versión de add
podría tomar el valor encontrado en un registro del procesador y agregarlo al valor de otro, otra versión podría agregar el valor encontrado en la memoria a un registro, mientras que otra podría agregar el valor en una ubicación de memoria a otra ubicación de memoria. Cada una de estas instrucciones toma una cantidad diferente de bytes para representarla en la memoria, lo que significa que toman diferentes cantidades de tiempo para obtenerlas, pueden requerir múltiples viajes a través de la interfaz de memoria para recopilar valores, etc. Esto complica enormemente la lógica de pipeline. Uno de los objetivos del concepto de diseño del chip RISC era eliminar estas variantes para que la lógica de pipeline se simplificara, lo que conduce al pipeline RISC clásico que completa una instrucción por ciclo.
Sin embargo, existe un problema que surge en los sistemas de pipeline que puede reducir el rendimiento. Esto ocurre cuando la siguiente instrucción puede cambiar dependiendo de los resultados de la última. En la mayoría de los sistemas, esto sucede cuando se produce una bifurcación . Por ejemplo, considere el siguiente pseudocódigo:
arriba: Leer un número de la memoria y almacenarlo en un registro leer otro número y almacenarlo en un registro diferente Añade los dos números en un tercer registro escribe el resultado en la memoria Leer un número de la memoria y almacenarlo en otro registro ...
En este caso, el programa es lineal y se puede segmentar fácilmente. Tan pronto como read
se ha leído la primera instrucción y se está decodificando, read
se puede leer la segunda instrucción desde la memoria. Cuando la primera se mueve para ejecutarse, se add
está leyendo desde la memoria mientras read
se decodifica la segunda, y así sucesivamente. Aunque todavía se necesitan la misma cantidad de ciclos para completar la primera read
, cuando se completa, el valor de la segunda está listo y la CPU puede agregarlos inmediatamente. En un procesador no segmentado, las primeras cuatro instrucciones tardarán 16 ciclos en completarse, en uno segmentado, solo se necesitan cinco.
Ahora consideremos lo que ocurre cuando se agrega una rama:
arriba: Leer un número de la memoria y almacenarlo en un registro leer otro número y almacenarlo en un registro diferente Añade los dos números en un tercer registro Si el resultado en el tercer registro es mayor que 1000, entonces regresa al inicio: (si no es así) escribe el resultado en la memoria Leer un número de la memoria y almacenarlo en otro registro ...
En este ejemplo, el resultado de la comparación en la línea cuatro hará que cambie la "próxima instrucción"; a veces será la siguiente write
a la memoria y, a veces, será la read
desde la memoria en la parte superior. La secuencia de comandos del procesador normalmente ya habrá leído la siguiente instrucción, la write
, para cuando la ALU haya calculado qué camino tomará. Esto se conoce como riesgo de bifurcación . Si tiene que volver a la parte superior, la write
instrucción debe descartarse y, read
en su lugar, leerse la instrucción desde la memoria. Esto requiere un ciclo de instrucción completo, como mínimo, y da como resultado que la secuencia de comandos esté vacía durante al menos el tiempo de una instrucción. Esto se conoce como "bloqueo de secuencia de comandos" o "burbuja" y, según la cantidad de bifurcaciones en el código, puede tener un impacto notable en el rendimiento general.
Una estrategia para lidiar con este problema es usar una ranura de retardo , que se refiere a la ranura de instrucción después de cualquier instrucción que necesite más tiempo para completarse. En los ejemplos anteriores, la instrucción que requiere más tiempo es la bifurcación, que es por lejos el tipo más común de ranura de retardo, y a estas se las conoce más comúnmente como ranura de retardo de bifurcación .
En las primeras implementaciones, la instrucción que seguía a la bifurcación se llenaba con una operación nula o NOP
simplemente para completar la secuencia de comandos y garantizar que la sincronización fuera la correcta, de modo que cuando se NOP
hubiera cargado desde la memoria, la bifurcación estuviera completa y el contador del programa pudiera actualizarse con el valor correcto. Esta solución simple desperdicia el tiempo de procesamiento disponible. En cambio, las soluciones más avanzadas intentarían identificar otra instrucción, normalmente cercana en el código, para colocarla en la ranura de demora de modo que se pudiera realizar un trabajo útil.
En los ejemplos anteriores, la read
instrucción al final es completamente independiente, no depende de ninguna otra información y se puede ejecutar en cualquier momento. Esto la hace adecuada para ubicarla en la ranura de retardo de bifurcación. Normalmente, esto lo manejaría automáticamente el programa ensamblador o el compilador , que reordenaría las instrucciones:
Leer un número de la memoria y almacenarlo en un registro leer otro número y almacenarlo en un registro diferente Añade los dos números en un tercer registro Si el resultado en el tercer registro es mayor a 1000, entonces regresa al inicio Leer un número de la memoria y almacenarlo en otro registro (si no es así) escribe el resultado en la memoria ...
Ahora, cuando la rama se está ejecutando, sigue adelante y ejecuta la siguiente instrucción. Cuando la instrucción se lee en el procesador y comienza a decodificar, el resultado de la comparación está listo y el procesador puede decidir qué instrucción leer a continuación, la read
de arriba o la write
de abajo. Esto evita perder tiempo y mantiene el flujo de trabajo completo en todo momento.
Encontrar una instrucción para llenar el espacio puede ser difícil. Los compiladores generalmente tienen una "ventana" limitada para examinar y pueden no encontrar una instrucción adecuada en ese rango de código. Además, la instrucción no puede depender de ninguno de los datos dentro de la rama; si una add
instrucción toma un cálculo anterior como una de sus entradas, esa entrada no puede ser parte del código en una rama que podría tomarse. Decidir si esto es cierto puede ser muy complejo en presencia de un cambio de nombre de registro , en el que el procesador puede colocar datos en registros distintos a los que especifica el código sin que el compilador sea consciente de ello.
Otro efecto secundario es que se necesita un manejo especial al gestionar puntos de interrupción en instrucciones, así como al avanzar durante la depuración dentro del intervalo de retraso de bifurcación. No se puede producir una interrupción durante un intervalo de retraso de bifurcación y se pospone hasta después del intervalo de retraso de bifurcación. [5] [6] La colocación de instrucciones de bifurcación en el intervalo de retraso de bifurcación está prohibida o en desuso. [7] [8] [9]
La cantidad ideal de ranuras de retardo de bifurcación en una implementación de canalización particular está determinada por la cantidad de etapas de la canalización, la presencia de reenvío de registros , en qué etapa de la canalización se calculan las condiciones de bifurcación, si se utiliza o no un búfer de destino de bifurcación (BTB) y muchos otros factores. Los requisitos de compatibilidad de software dictan que una arquitectura no puede cambiar la cantidad de ranuras de retardo de una generación a la siguiente. Esto inevitablemente requiere que las implementaciones de hardware más nuevas contengan hardware adicional para garantizar que se respete el comportamiento arquitectónico a pesar de que ya no sea relevante.
Las ranuras de retardo de bifurcación se encuentran principalmente en arquitecturas DSP y arquitecturas RISC más antiguas . MIPS , PA-RISC (se puede especificar bifurcación retardada o no retardada), [10] ETRAX CRIS , SuperH (las instrucciones de bifurcación incondicional tienen una ranura de retardo), [11] Am29000 , [12] Intel i860 (las instrucciones de bifurcación incondicional tienen una ranura de retardo), [13] MC88000 (se puede especificar bifurcación retardada o no retardada), [14] y SPARC son arquitecturas RISC que tienen cada una una ranura de retardo de bifurcación única; PowerPC , ARM , Alpha , V850 y RISC-V no tienen ninguna. Las arquitecturas DSP que tienen cada una una ranura de retardo de bifurcación única incluyen μPD77230 [15] y el DSP VS. El DSP SHARC y MIPS-X utilizan una ranura de retardo de bifurcación doble; [16] Un procesador de este tipo ejecutará un par de instrucciones después de una instrucción de bifurcación antes de que la bifurcación surta efecto. Tanto el TMS320C3x [17] como el TMS320C4x [8] utilizan una ranura de retardo de bifurcación triple. El TMS320C4x tiene bifurcaciones tanto no retardadas como retardadas. [8]
El siguiente ejemplo muestra bifurcaciones retrasadas en lenguaje ensamblador para el DSP SHARC, incluido un par después de la instrucción RTS. Los registros R0 a R9 se borran a cero en orden por número (el registro borrado después de R6 es R7, no R9). Ninguna instrucción se ejecuta más de una vez.
R0 = 0; CALL fn (DB); /* llamar a una función, debajo en la etiqueta "fn" */ R1 = 0; /* primer intervalo de retardo */ R2 = 0; /* segundo intervalo de retardo */ /***** discontinuidad aquí (la LLAMADA tiene efecto) *****/ R6 = 0; /* el CALL/RTS regresa aquí, no en "R1 = 0" */ SALTO final (DB); R7 = 0; /* primer intervalo de retardo */ R8 = 0; /* segundo intervalo de retardo */ /***** discontinuidad aquí (el SALTO tiene efecto) *****/ /* las siguientes 4 instrucciones se llaman desde arriba, como función "fn" */función: R3 = 0; RTS (DB); /* regresar al llamador, más allá de los intervalos de retardo del llamador */ R4 = 0; /* primer intervalo de retardo */ R5 = 0; /* segundo intervalo de retardo */ /***** discontinuidad aquí (el RTS entra en vigor) *****/fin: R9 = 0;
Una ranura de retardo de carga es una instrucción que se ejecuta inmediatamente después de una carga (de un registro desde la memoria) pero no ve, y no necesita esperar, el resultado de la carga. Las ranuras de retardo de carga son muy poco comunes porque los retrasos de carga son altamente impredecibles en el hardware moderno. Una carga puede ser satisfecha desde la RAM o desde una memoria caché, y puede ser ralentizada por la contención de recursos. Los retrasos de carga se observaron en los primeros diseños de procesadores RISC. El ISA MIPS I (implementado en los microprocesadores R2000 y R3000 ) sufre este problema.
El siguiente ejemplo es un código de ensamblaje MIPS I, que muestra una ranura de retardo de carga y una ranura de retardo de rama.
lw v0 , 4 ( v1 ) # carga la palabra desde la dirección v1+4 en v0 nop # ranura de retardo de carga desperdiciada jr v0 # salta a la dirección especificada por v0 nop # ranura de retardo de rama desperdiciada