Resumen del libro "Computer Architecture a Quantitative Approach" Capítulo 2 - Principios del Set de Instrucciones 2.1 - Introducción El set de instrucciones es una porción de la computadora visible para el programador o el compilador. Este capítulo analiza los cinco puntos: 1. 2. 3. 4. 5. Taxonomías de distintos sets de instrucciones. Alternativas, ventajas y desventajas. Mediciones de los sets de instrucciones, independientes de los distintos sets. Set de instrucciones para DSP (Digital Signal Processor) y Aplicaciones Embebidas. Conflictos entre lenguajes, compiladores y sets de instrucciones. Análisis del set de instrucciones MIPS, para arquitectura RISC. Los benchmarks son una serie de pruebas/mediciones de las distintas arquitecturas. Las aplicaciones se dividen en tres áreas distintas: Computadoras de escritorio: Enfatiza la performance de programas con números enteros y punto flotante, con poco incapié en el tamaño del programa y consumo de energía. Servidores: usados principalmente para bases de datos y aplicaciones web. La performance de operaciones con punto flotante es menos importante que con enteros y cadenas de texto (aunque todos tienen instrucciones de punto flotante). Aplicaciones embebidas: valoran costo y potencia (consumo de energía), por lo que el tamaño del código es importante (usar menos memoria es más barato y consume menos). Instrucciones de punto flotante pueden ser opcionales. Los DSP y Media Processors enfatizan performance en tiempo real. Muchas veces se pone como objetivo el peor escenario para segurar una ganartía para tiempo real. Si bien suelen utilizar quirks al programar determinados kernels, las aplicaciones embebidas suelen ser cada vez más de uso general (hay sistemas operativos y funcionalidades más avanzadas). 2.2 Clasificando los Sets de Instrucciones El tipo de almacenamiento interno en un procesador es la diferenciación más básica. Los típicos son stack, acumulador y set de registros. Los operadores pueden ser explícitos o implícitos. Los operadores están implícitos en la arquitectura de stack (TOS: Top of Slack). En la arquitectura de acumulador, un operador está implícito en el acumulador. La arquitectura de Registros de propósito general sólo tiene operadores explícitos, sea memoria o registros. Según el tipo de arquitectura y la operación, los operadores pueden acceder directamente a la memoria o puede que necesiten cargar los datos primero a un registro. Hay dos tipos de arquitectura de registros. Un tipo puede acceder a la memoria como parte de una instrucción (llamada arquitectura registro-memoria). La otra puede acceder a la memoria sólo con instrucciones de load y store (llamada arquirectura de load-store o registro-registro). Una tercer arquitectura no muy utilizada puede mantener todas sus operaciones en memoria y es llamada arquitectura memoria-memoria. Anteriormente todas usaban arquitecturas de stack o acumulador. Actualmente usan arquitecturas de registros load-store. Las razones son que los registros son más rápidos que la memoria y más eficientes para un compilador, debido, por ejemplo, al pipeline. Más importante aún, los registros pueden almacenar variables, lo que reduce el tráfico a/desde memoria, aumenta la velocidad del programa y la densidad de código mejora. Los compiladores mejoran cuando aumenta la cantidad de registros de propósito general. El dominio del código optimizado a mano en las aplicaciones para DSP tiende a mantener muchos registros de uso específico. Las arquitecturas GPR (General Purpose Register) se dividen según dos características importantes. Ambas están relacionados a la naturaleza de las típicas operaciones aritmético-lógicas (ALU): 1. Si las instrucciones de la ALU tienen dos o tres operandos. En la de tres operandos, el tercero es para almacenar el resultado. En la de dos, uno es tanto un dato como el resultado. 2. La cantidad de operandos que pueden ser direccionados a la memoria. Este valor va de cero a tres. De las 7 combinaciones posibles, se destacan tres: Registro-Registro (0, 3) Registro-Memoria (1, 2) Memoria-Memoria (2, 2) / (3, 3) (...) Apéndice A - Pipelining: Basic and Intermediate Concepts A.1 - Introducción Que es Pipeline? Pipeline es una técnica de implementación donde múltiples instrucciones se solapan durante la ejecución, tomando ventaja del paralelismo que existe entre las acciones necesarias para ejecutar una intrucción. Cada etapa del pipeline (llamada pipe stage o pipe segment) completa una parte de la instrucción. Las bases de un set de instrucciones RISC Todas las operaciones en datos se realizan en registros Las únicas operaciones que intervienen con la memoria son las operaciones de load/store Los formatos de instrucción son pocos en números con todas las operaciones siendo típicamente de un sólo tamaño Estas simples propiedades llevan a una dramática simplificación en la implementación del pipeline. El set de instrucciones MIPS provee 32 registros (aunque el registro 0 siempre tiene el valor 0). Las instrucciones se dividen en tres clases distintas: Instrucciones ALU: Toman dos registros o un registro y un valor inmediato, opera con ellos y graba el resultado en un tercer registro. Operaciones (ej): AND, SUB y operaciones lógicas. Instrucciones de load/store: Toman un registro de origen (registro base) y un valor inmediato (offset) como operandos. La suma de ambos (dirección efectiva) es usada como una dirección de memoria. Un segundo registro opera como destino de los datos cargados desde memoria (instrucciones load) o como origen de los datos para guardar en memoria (instrucciones store). Instrucciones de Branch y Jump: Los branch son tranferencias de control condicionales. Un set de comparaciones entre un par de registros y/o cero determinan si se hace un salto o no. El destino del salto se obtiene sumando un offset al PC actual. Implementación sencilla de un set de instrucciones RISC Cada instrucción del subconjunto RISC puede ser implementada en a lo sumo 5 ciclos de reloj: 1. Instruction Fetch Cycle (IF): Envía el PC a la memoria y trae la instrucción actual desde la memoria. Actualiza el PC incrementándolo en 4. 2. Instruction Decode/Register Fetch Cycle (ID): Decodifica la instrucción y lee los registros de origen de datos correspondientes. Calcula la posible dirección de destino del Branch. La decodificación se lleva a cabo en paralelo a la lectura de registros, lo que es posible gracias a que los especificadores de registros están en una posición fija de la instrucción (fixed-field decoding). 3. Execution/Effective Address Cycle (EX): La ALU opera sobre los operandos indicados, mediante una de las tres operaciones: Referencia a memoria: La ALU suma la dirección base al offset, para obtener la dirección efectiva. Instrucción ALU Registro-Registro: La ALU realiza una operación sobre los registros, según el código de la instrucción (opcode). Instrucción ALU Registro-Inmediato: La ALU realiza una operación entre un registro y un valor inmediato. En una arquitectura load/store la dirección efectiva y el ciclo de ejecución puede ser combinada en un único ciclo de reloj, ya que ninguna instrucción necesita simultaneamente calcular una dirección y realizar una operación sobre la misma. 4. Memory Access (MEM): Si la instrucción es de lectura (load), la memoria realiza una lectura usando la dirección efectiva. Si es de escritura (store) entonces la memoria escribe los datos del segundo registro. 5. Write-Back Cycle (WB): En instrucciones ALU Registro-Registro e instrucciones Load el resultado se escribe en el registro correspondiente, sea que viene de la memoria (load) o de la ALU. Pipeline clásico de 5 etapas para un procesador RISC La implementación debe asegurarse que el solapamiento de instrucciones no genere conflictos entre si. Para ello se tienen en cuenta tres cosas: Primero, se manejan por separado la memoria para datos e instrucciones, haciendo uso de un caché de datos y otro de instrucciones. Segundo, el archivo de registros es usado en dos etapas: una para leer durante el ID y otra para escribir durante el WB. Se necesitan dos lecturas y una escritura en cada ciclo de reloj. Para manejar las lecturas y escrituras en un mismo registro, se realiza la escritura en la primera mitad del ciclo del reloj y se lo lee durante la segunda mitad. Tercero, el PC debe incrementarse en cada ciclo, para el próximo IF, pero deben tenerse en cuenta los posibles saltos de un branch, durante la etapa de ID. Si bien es crítico asegurarse que las instrucciones en un pipeline no intenten usar un mismo recurso de hardware a la misma vez, es necesario asegurarse que las instrucciones durante las distintas etapas del pipeline no interfieran entre si. Esto se puede lograr introduciendo registros de pipeling entre las sucesivas etapas, de modo que al finalizar un ciclo de reloj todos los resultados de una determinada etapa son guardados en un registro que es usado como entrada para la próxima etapa. Los registros son nombrados según las etapas que interconectan (ID/IF, IF/RF, RF/EX, EX/MEM, MEM/WB). Problemas básicos de performance en un pipeline El pipeline incrementa el rendimiento del CPU (cantidad de instrucciones realizadas por segundo), pero no reduce el tiempo de ejecución de una instrucción individual. El tiempo requerido para mover una instrucción a lo largo del pipeline es un ciclo de reloj. Como todas las etapas se ejecutan a la vez, la duración de un ciclo de reloj está determinada por la etapa más lenta. La diferencias de duraciones entre estas y un overhead del pipeline (generado por la inclusión de los registros de pipeline), incrementan el tiempo de ejecución de una instrucción, aunque igualmente, mejora el rendimiento. A.2 - Riesgos de Pipeline (Pipeline hazards) Hay situaciones, llamadas hazards (riesgos), que previenen que la próxima instrucción del flujo de instrucciones pueda ser ejecutada durante su ciclo de reloj correspondiente. Los Riesgos reducen la performance. Hay tres tipos de riesgos: Riesgos estructurales: surgen de conflictos donde el hardware no puede soportar todas las combinaciones de instrucciones simultaneamente (solapadas). Riesgos de datos: cuando una instrucción depende del resultado de una instrucción previa que se ejecuta en paralelo. Riesgos de control: originados por instrucciones como los branch que cambian el PC. Estos obstáculos pueden hacer necesario detener (stall) el pipeline. Cuando una instrucción es demorada, las instrucciones posteriores a ésta también son congeladas. Las instrucciones previas deben continuar ejecutándose. Performance de pipelines con stalls Una demora hace que la performance del pipeline decaiga de la ideal. Riesgos estructurales La ejecución de instrucciones solapadas requiere implementar pipeline en unidades funcionales y duplicación de recursos. Cuando una combinación de instrucciones no puede ser ejecutada correctamente por problemas de recursos, se dice que tiene un Riesgo Estructural (Conflicto Estructural). Generalmente ocurren cuando una unidad funcional no está completamente tuneleada (y no se puede procesar una serie de instrucciones a una por ciclo de reloj) o cuando un recurso no fue duplicado lo suficiente. Cuando una secuencia de instrucciones se encuentra con este conflicto, el pipeline detiene una de las instrucciones hasta que la unidad requerida se libere.Cuando esto ocurre, se genera una burbuja. Esta demora hace que el CPI decaiga del valor ideal de 1. El efecto de esa burbuja es ocupar una etapa ciclo, a medida que avanza por el pipeline. ¿Por qué un diseñador permitiría un riesgo estructural? Principalmente, para reducir el costo de la unidad. Riesgos de Datos Ocurren cuando un pipeline intercambia el orden de acceso de lectura/escritura a un operando. Ejemplo: ADD R1, R2, R3 SUB R4, R1, R5 AND R6, R1, R7 OR R8, R1, R9 XOR R10,R1, R11 La instrucción ADD escribe el valor de R1 en la etapa WB, pero la instrucción SUB lee el valor durante su fase ID. Este caso es un Riesgo de Datos. La instrucción SUB va a leer un valor erroneo y tratar de usarlo. La operación AND también estaría usado un dato erroneo. Sin embargo las instrucciones OR y XOR no incurrirían en un riesgo de datos. Minimizing Data Hazard Stalls by Forwarding Forwarding (aka bypassing or short-circuiting) funciona de la siguiente manera: 1. El resultado de la ALU, tanto del registro EX/MEM como del registro MEM/WB, es alimentado de vuelta a la entrada de las ALUs. 2. Si el hardware de forwarding detecta que la instrucción previa escribió un registro usado en la operación actual, la lógica de control selecciona el resultado de la operación previa (la salida de la ALU) como la entrada a la ALU para esta operación (en vez del valor leído del registro). Riesgos de Datos que requieren demoras Desafortunadamente no todas las Riesgos de Datos pueden ser evitados mediante forwarding. Analizar el siguiente caso: LD R1, 0(R2) SUB R4, R1, R5 AND R6, R1, R7 OR R8, R1, R9 La instrucción LD no tienen la información hasta el final de su ciclo 4 (ciclo MEM), mientras que la instrucción SUB necesita tener el dato al principio del mismo ciclo. Por ende, el riesgo de datos generado por una instrucción Load no puede ser eliminado completamente con simple hardware. La instrucción Load tiene un delay o latencia. Se necesita modificar el pipeline para agregarle un circuito de bloqueo (pipeline interlock) para preservar la correcta ejecución. Este circuito detecta un riesgo de datos y congela el pipeline (stall), generandose una burbuja. Debido a esta demora, la instrucción AND no necesita forwarding (los datos los toma desde el registro) y la instrucción OR no necesita ningún tipo de forwarding. Riesgo de Salto/Control Los riesgos de control pueden causar una mayor pérdida de performance que los riesgos de datos. Cuando un branch es ejecutado, puede o no cambiar el PC. Si el branch cambia el PC a su dirección de destino, se dice que el salto es tomado (taken). En caso contrario, se dice que el salto es no tomado (not taken). La solución más sencilla para tratar con los saltos sería: luego de detectar que la instrucción es de salto (durante la fase de ID), repetir el IF de la instrucción posterior al branch, pero usando la dirección de destino. De esta manera, el primer ciclo IF es esencialmente un stall. Un stall por cada branch genera decremento en la performance de entre un 10% y 30%. Reduciendo la penalidad del salto Hay varios métodos para tratar con los stall causados por las instrucciones de branch. Se puede analizar cuatro esquemas distintos, a la hora de compilar, en donde el compilador podrá tratar de minimizar la penalidad de la demora haciendo uso del conocimiento del esquema del hardware usado y el comportamiento de los saltos. El esquema más sencillo es congelar (freeze) o dejar fluír (flush) el pipeline, manteniendo o eliminando cualquier instrucción después del branch, hasta que el destino del salto se sepa. La ventaja de este método se aprecia en su simpleza, tanto en hardware como en software. La penalidad es fija y no puede ser reducida por software. Más performante y apenas más complejo es tratar a cada salto como no tomado, simplemente dejando que el hardware continúe como si el salto no fuera ejecutado. Se debe tener cuidado de no cambiar el estado del procesador hasta que el destino del branch sea conocido.