Story Transcript
11. Estructura y Función del Procesador Puntos Clave: •
•
Un procesador incluye tanto registros visibles al usuario como registros de control/estatus. Los primeros pueden ser referenciados, implícitamente o explícitamente, por las instrucciones. Los registros visibles al usuario pueden ser de propósito general o tener un uso especial, como para números de punto fijo o punto flotante, direcciones, índices o punteros de segmento. Los registros de control y estatus se utilizan para controlar el funcionamiento del procesador. Un ejemplo obvio es el contador de programa. Otro ejemplo importante es la palabra de estatus de programa (PSW) que contiene una variedad de bits de estado y condición. Estos bits incluyen algunos que reflejan el resultado de la operación aritmética más reciente, bits de habilitación de interrupción y un indicador de si el procesador se encuentra trabajando en modo supervisor o modo usuario. Los procesadores hacen uso de la segmentación y entubamiento de instrucciones para acelerar la ejecución. En esencia, la segmentación consiste en dividir el ciclo de instrucción en un número de etapas separadas que ocurren en secuencia, como puede ser la obtención de la instrucción, decodificación de la instrucción, determinar las direcciones de los operandos, obtener los operandos, ejecutar la instrucción y escribir el resultado. Las instrucciones se mueven a través de estas etapas, como en una linea de ensamblaje, de manera que en teoría, cada etapa puede estar trabajando en una instrucción diferente al mismo tiempo. La ocurrencia de saltos y la dependencia entre instrucciones complica el diseño y el uso de la segmentación.
11.1 Organización del procesador Para entender la organización del procesador, consideremos los requerimientos del mismo, las cosas que debe hacer: • Obtener la instrucción (fetch instruction): el procesador lee una instrucción de la memoria. • Interpretar la instrucción (decode instruction): la instrucción es decodificada para determinar qué acción es requerida. • Obtener datos (fetch data): la ejecución de una instrucción puede requerir de datos de la memoria o un módulo I/O. • Procesar datos (process data): la ejecución de una instrucción puede requerir realizar alguna operación aritmética o lógica sobre los datos. • Escribir datos (write data): los resultados de la ejecución pueden requerir escribir datos a memoria o a un módulo I/O. Para hacer estas cosas, debe ser claro que el procesador necesita almacenar ciertos datos de forma temporal. Debe recordar la ubicación de la última instrucción de manera que pueda saber cómo obtener la siguiente instrucción. Necesita almacenar instrucciones y datos temporalmente mientras una instrucción se encuentra en ejecución. En otras palabras, el procesador necesita de memoria interna. La figura a la derecha muestra una vista simplificada del procesador, indicando su conexión con el resto del sistema a través del bus del sistema. Recordemos que los componentes principales de un procesador son la unidad aritmética lógica (ALU) y la unidad de control (CU). La ALU hace los cálculos o procesamiento de datos. La CU controla el movimiento de datos e instrucciones desde y hacia la ALU. Adicionalmente, la figura muestra la memoria interna del procesador, que consiste en un conjunto de localidades de almacenamiento llamadas registros.
La figura abajo muestra una vista ligeramente más detallada del procesador. Se muestran los caminos de control y transferencia de datos, incluyendo el bus interno del procesador. Este bus es necesario para transferir datos entre varios registros y la ALU porque la ALU sólo opera sobre datos que se encuentran en la memoria interna del procesador. La figura también muestra los elementos básicos de la ALU. Note la similitud entre la estructura interna de una computadora y la estructura interna del procesador. 11.2 Organización de los registros Como se discutió previamente, un sistema de computadora emplea una jerarquía de memoria. En los niveles más altos de la jerarquía, la memoria es más rápida, más pequeña y más cara (por bit). Dentro del procesador existe un conjunto de registros que funcionan como un nivel de memoria por encima de la memoria principal y la memoria caché. Los registros del procesador realizan dos roles: • Registros visibles al usuario: permiten al programador de lenguaje máquina minimizar las referencias a memoria principal. • Registros de control y estatus: usados por la unidad de control para regular la operación del procesador; los programas privilegiados del sistema operativo los utilizan para controlar la ejecución de los programas. No existe una separación limpia y clara de los registros entre estas dos categorías. Por ejemplo, en algunas máquinas el contador de programa es visible, pero en muchas otras no. Registros visibles al usuario Un registro visible al usuario puede ser referenciado dentro de una instrucción de lenguaje máquina. Se pueden caracterizar en las siguientes categorías: • Propósito general • Datos • Dirección • Códigos de condición Los registros de propósito general pueden ser asignados a una variedad de funciones por el programador. Algunas veces su uso dentro del conjunto de instrucciones es ortogonal a la operación, ésto provee un registro de verdadero uso general. Sin embargo, muchas veces existen restricciones. Por ejemplo, pueden existir registros dedicados para las operaciones de punto flotante o de stack. En algunos casos, los registros de propósito general pueden ser utilizados para funciones de direccionamiento (e.j. Direccionamiento indirecto de registro, por desplazamiento). En otros casos, existe una separación parcial entre registros de datos y registros de dirección. Los registros de datos sólo pueden ser utilizados para almacenar datos y no se pueden emplear en el cálculo de la dirección de un operando. Los registros de dirección pueden a su vez ser dedicados a un solo modo de direccionamiento o pueden emplearse en varios. Algunos ejemplos son: • Punteros de segmento: en una máquina con direccionamiento segmentado, un registro de segmento guarda la dirección base del segmento.
• •
Registros índice: se utilizan para el direccionamiento indexado y pueden ser autoindexados. Punteros de stack: se existe direccionamiento por stack visible al usuario, entonces, típicamente, existe un registro dedicado que siempre apunta al tope del stack. Esto permite el direccionamiento implícito, es decir, push, pop, y otras instrucciones de stack no necesitan especificar un operando.
Existen varias cuestiones de diseño que se deben discutir. Una cuestión importante es el utilizar registros de propósito general o especializar su uso. Con el uso de registros especializados, generalmente puede ir implícito en el opcode el tipo de registro al que cierto operando se refiere. El especificador de operando únicamente debe identificar un conjunto de registros especializados en lugar de uno de entre todos los registros. Por otra parte, esta especialización limita la flexibilidad del programador. Otra cuestión de diseño es el número de registros, ya sean de propósito general o de datos/direcciones, que se proveen. Ésto afecta el diseño del conjunto de instrucciones puesto que más registros implican más bits de especificación. Como se mencionó antes, entre 8 y 32 registros parece ser el valor óptimo. Menos (de 8) registros resultan en más referencias a memoria; más (de 32) registros no reducen de manera notable las referencias. Finalmente, existe la cuestión de la longitud de los registros. Los registros que almacenan direcciones, obviamente, deben ser al menos tan grandes como para almacenar la dirección más larga. Los registros de datos deben ser capaces de almacenar valores de la mayoría de los tipos de datos. Algunas máquinas permiten que dos registros contiguos se utilicen como uno solo para almacenar valores de doble longitud (doubles). Una categoría final de registros, que es al menos parcialmente visible al usuario, almacenan los códigos de condición (también llamados banderas). Los códigos de condición son bits establecidos por el hardware del procesador como resultado de las operaciones. Por ejemplo, una operación aritmética puede producir un resultado positivo, negativo, cero o desbordamiento. Adicionalmente al resultado mismo almacenado en un registro o en memoria, se establece también un código de condición. El código puede subsecuentemente ser probado como parte de una operación de bifurcación condicional. Los códigos de condición se reúnen en uno o más registros. Usualmente, forman parte de un registro de control. Generalmente, las instrucciones máquina permiten que estos bits sean leídos implícitamente, pero el programador no puede alterarlos. Registros de control y estatus Existen una variedad de registros del procesador que pueden ser empleados para controlar la operación del procesador. La mayoría de ellos no son visibles al usuario. Algunos pueden ser visibles a ciertas instrucciones máquina ejecutadas en modo de control o de sistema operativo. Cuatro registros son esenciales para la ejecución de instrucciones: • Contador de programa (PC, Program Counter): contiene la dirección de una instrucción a ser obtenida. • Registro de instrucción (IR, Instruction Register): contiene la última instrucción obtenida. • Registro de dirección de memoria (MAR, Memory Address Register): contiene la dirección de una localidad en memoria. • Registro de buffer de memoria (MBR, Memory Buffer Register): contiene una palabra de datos a ser escrita a memoria o la última palabra leída de memoria. No todos los procesadores tienen registros internos designados como MAR y MBR, pero es necesario algún tipo equivalente de mecanismo para que los bits a ser transferidos al bus del sistema sean guardados y los datos leídos del bus sean almacenados temporalmente. Típicamente el procesador actualiza PC después de cada obtención de instrucción de manera que éste siempre apunta a la siguiente instrucción a ser ejecutada. Una instrucción de salto o de omisión también modificará los contenidos del PC. La instrucción obtenida se carga en IR, donde se analizan el opcode y los especificadores de operando. Los datos se intercambian con la memoria utilizando MAR y MBR. En un sistema organziado por bus, MAR se conecta con el bus de direcciones, y el MBR se conecta directamente con el bus de datos. Los registros visibles al usuario intercambian datos con MBR.
Los cuatro registros mencionados se utilizan para el translado de datos entre el procesador y memoria. Dentro del procesador, los datos deben ser presentados a la ALU. La ALU puede tener acceso directo al MBR y los registros visibles al usuario. Alternativamente, pueden existir registros de buffering adicional los cuales sirvan como registros de entrada y salida para la ALU e intercambian información con MBR y los registros visibles al usuario. Muchos diseños de procesador incluyen un registro o un conjunto de registros, conocidos como la palabra de estatus de programa (PSW, program status word) que contiene información de estatus. El PSW típicamente contiene códigos de condición además de otra información. Algunos campos o banderas comunes son los siguientes: • Signo: contiene el bit de signo del resultado de la última operación aritmética. • Cero: se establece cuando el resultado es 0. • Acarreo: se establece si el resultado de una operación lleva acarreo. • Igual: establecido si una operación lógica de comparación da la igualdad. • Overflow: indica el sobreflujo aritmético. • Habilitación de interrupciones: utilizado para habilitar o deshabilitar las interrupciones. • Supervisor: indica si el procesador se encuentra en modo privilegiado o en modo usuario. Ciertas instrucciones sólo pueden ser ejecutadas en modo supervisor así como ciertas áreas de la memoria. Un número de otros registros relacionados con el estado y el control pueden encontrarse en diseños particulares. Puede existir un puntero a un bloque de memoria que contiene información adicional. Si se utiliza un stack para implementar ciertas funciones (e.j. Llamadas a procedimientos), entonces un puntero al stack del sistema es necesario. Un puntero a la tabla de páginas es usado en un sistema de memoria virtual. Finalmente, se pueden utilizar registros para el cotnrol de operaciones I/O. Una cuestión importante de diseño es la colocación de la información de control entre los registros y la memoria. Es común dedicar los primeros cientos o miles de palabras de memoria a propósitos de control. El diseñador debe decidir cuánta información de control debe haber en los registros y cuánta en memoria. El intercambio usual entre velocidad y costo es relevante. Ejemplo de organización de registros en un microprocesador Es instructivo examinar y comparar la organización de registros entre dos sistemas. En esta sección se observan dos microprocesadores de 16 bits que fueron diseñados casi al mismo tiempo: el Motorola MC68000 y el Intel 8086. La figura siguiente muestra la organización de los registros de cada uno; los registros internos, como un registro de dirección de memoria, no se muestran.
El MC68000 particiona sus registros de 32-bits en 8 registros de datos y 9 registros de dirección. Los registros de datos se utilizan primordialmente para la manipulación de datos pero también se utilizan en el direccionamiento como registros índice. El ancho de los registros permite operaciones de 8, 16 y 32 bits, determinado por el opcode. Los registros de dirección contienen direcciones de 32 bits; dos de estos registros también se utilizan como apuntadores de stack, uno para los usuarios y uno para el sistema operativo, dependiendo del modo de ejecución. Ambos registros se numeran como 7, pero sólo pueden ser usados uno a la vez. El MC68000 también incluye un contador de programa de 32 bits y un registro de estatus de 16 bits. El equipo de motorola quería un conjunto de instrucciones muy regular, sin registros de propósito especial. La preocupación por la eficiencia del código los llevó a dividir los registros en dos componentes funcionales, ahorrando un bit por cada especificador de registro. El Intel 8086 toma un enfoque diferente para su organización de registros. Cada registro tiene un propósito especial, aunque algunos registros también son utilizables como de propósito general. El 8086 contiene cuatro registros de 16 bits que son direccionables por byte o por 16-bits, y cuatro registros de 16 bits como apuntadores y registros índice. Los registros de datos pueden ser usados como de propósito general en ciertas instrucciones. En otras, los registros se utilizan implícitamente. Por ejemplo, una instrucción de multiplicación siempre utiliza el acumulador. Los cuatro registros apuntadores también son utilizados implícitamente en un número de operaciones; cada uno contiene un desplazamiento de segmento. Existen también 4 registros de segmento de 16 bits. Tres de los cuatro se utilizan de manera dedicada e implícita, para apuntar al segmento de la instrucción actual, a un segmento que contiene datos y un segmento que contiene un stack, respectivamente. Estos usos dedicados e implícitos proveen codificación compacta con el costo de flexibilidad reducida. El 8086 también incluye un apuntador de instrucción y un conjunto de banderas de estatus y control de 1 bit. El punto de esta comparación debería ser claro. No hay una filosofía universalmente aceptada sobre la mejor manera de organizar los registros del procesador. Es cuestión de juicio y gusto del diseñador. Un segundo punto es ilustrado en la parte derecha de la figura, la cual muestra la organización de los registros visibles al usuario para el Intel 80386, el cual es un microprocesador de 32 bits diseñado como una extensión del 8086. El 80386 utiliza registros de 32 bits. Sin embargo, para proveer compatibilidad con los programas escritos para el 8086, el 80386 retiene la organización original pero embebida dentro de la nueva organización.
11.3 El Ciclo de la Instrucción
• • •
Anteriormente se describió el ciclo de la instrucción incluyendo las siguientes etapas: Traída (fetch): leer la siguiente instrucción de memoria y hacia el procesador. Ejecución: interpretar el código de operación y efectuar la operación indicada. Interrupción: si las interrupciones están habilitadas y ha ocurrido una interrupción, salvar el estado del proceso actual y atender la interrupción.
Ahora con el conocimiento de los modos de direccionamiento se puede introducir una etapa adicional, el ciclo de indirección. La ejecución de una instrucción puede involucrar el traer uno o más operandos de memoria, cada uno de los cuales requiere un acceso a memoria. Además, si se utiliza direccionamiento indirecto, se necesitan accesos a memoria adicionales. Estas obtenciones de direcciones indirectas se pueden considerar como una etapa adicional. El resultado se aprecia en la figura a la derecha donde la línea principal de actividad consiste en la alternancia entre las etapas de traída y ejecución. Una vez que se obtiene una instrucción, se examina para determinar si involucra direccionamiento indirecto. De ser así, los operandos requeridos se obtienen mediante indirección. Al terminar la ejecución de la instrucción, se puede procesar una interrupción antes de obtener la siguiente instrucción. Un vistazo más profundo al ciclo de la instrucción se observa en la figura siguiente. Una vez obtenida la instrucción, se deben identificar sus operandos. Cada operando de entrada que se encuentre en memoria es obtenido de acuerdo a su modo de direccionamiento, lo cual puede necesitar direccionamiento indirecto. No es necesario obtener los operandos que referencían registros. Una vez que se ejecuta el opcode, un proceso similar se necesita para almacenar el resultado en memoria.
Flujo de Datos La secuencia exacta de eventos durante un ciclo de instrucción depende del procesador. Sin embargo, podemos, en términos generales, indicar lo que debe de ocurrir. Durante el ciclo de obtención (fetch cycle), se lee una instrucción de la memoria. PC contiene la dirección de la siguiente instrucción a ser obtenida. Esta dirección se mueve a MAR y se coloca en el bus de direcciones. La unidad de control hace una petición de lectura. El resultado de la operación se pone en el bus de datos y es copiado a MBR. Posteriormente se mueve a IR. Mientras tanto, se incrementa PC en 1, preparándolo para la siguiente traída. Una vez que termina el ciclo de obtención, la unidad de control examina los contenidos de IR para determinar si contiene algún especificador de operando que utiliza direccionamiento indirecto. De ser así, comienza un ciclo de indirección. Los N bits de la derecha de MBR, que contienen la referencia a la dirección, son transferidos al MAR. Entonces la unidad de control hace una petición de lectura para almacenar la dirección deseada del operando en MBR. Los ciclos indirecto y de obtención son simples y predecibles. El ciclo de ejecución toma muchas formas dependiendo de cuál de las muchas instrucciones se encuentra en IR. Este ciclo puede involucrar el transferir datos entre registros, leer o escribir de memoria o un dispositivo I/O, o una operación de la ALU. El ciclo de interrupción también es simple y predecible. El contenido actual de PC se deber guardar para que el procesador puede reanudar su actividad normal después de la interrupción. Así, el contenido de PC se transfiere al MBR para ser escrito en memoria. La localidad de memoria especial reservada para este propósito se carga en MAR por la unidad de control. Puede ser, por ejemplo, un apuntador al tope del stack. PC se carga con la dirección de la rutina de interrupción. Como resultado, el siguiente ciclo de instrucción comenzará por traer la primera instrucción de la rutina. 11.4 Segmentación de Instrucciones La segmentación de instrucciones es similar al uso de una línea de ensamblaje en una planta de producción. Una línea de ensamblaje toma ventaja del hecho que un producto pasa por varias etapas de producción, así se puede trabajar simultáneamente en varios productos que se encuentran en diversas etapas. Para aplicar este concepto a la ejecución de instrucciones se debe reconocer el hecho de que una instrucción también pasa por diversas etapas. Un enfoque simple es considerar dividir la instrucción en dos etapas únicamente: obtención y ejecución. Existen periodos durante la ejecución de una instruccción en que la memoria principal no está siendo accesada. Estos periodos pueden ser utilizados para obtener la siguiente instrucción en paralelo con la ejecución de la instrucción actual. La figura siguiente ilustra este enfoque: la segmentación consiste de dos etapas independientes; la primera etapa obtiene una instrucción y la almacena en un bufffer. Cuando la segunda etapa
está disponible, la primera etapa le pasa la instrucción del buffer. Mientras la segunda etapa se ocupa en ejecutar la instrucción, la primera etapa aprovecha los ciclos en los que la memoria está desocupada para obtener la siguiente instrucción. A esto se le conoce como pre-obtención (prefetch) de instrucciones.
Debería ser claro que este proceso acelera la ejecución de las instrucciones. Si las etapas de obtención y ejecución tuvieran una duración igual, el tiempo del ciclo de la instrucción se vería reducido a la mitad. Sin embargo esto es poco probable debido a dos factores: 1. El tiempo para la etapa de ejecución será generalmente más largo que para la etapa de obtención. La ejecución involucra leer y almacenar operandos de memoria y la realización de alguna operación. Así, la etapa de obtención tendrá que esperar algo de tiempo antes de que pueda liberar su buffer. 2. Una instrucción de bifurcación condicional hace que la dirección de la siguiente dirección a obtener sea desconocida. Así, la etapa de obtención deberá esperar hasta que reciba la dirección de la siguiente instrucción. El tratar de adivinar puede reducir la pérdida de tiempo, una regla simple puede ser: cuando se pase una instrucción de bifurcación condicional de la etapa de obtención a la etapa de ejecución, la etapa de obtención trae la siguiente instrucción de la memoria inmediatamente posterior a la instrucción de bifurcación. Así, si el salto no ocurre, no se perdió tiempo. Si el salto si ocurre, entonces la instrucción pre-obtenida debe ser desechada.
Para ganar mayor aceleración en la ejecución, la segmentación deberá tener más etapas. Consideremos la siguiente descomposición del proceso: • Obtención de Instrucción (FI, Fetch Instruction): leer en un buffer la siguiente instrucción esperada. • Decodificar la Instrucción (DI, Decode Instruction): determinar el opcode y los especificadores de operando. • Calcular los Operandos (CO, Calculate Operands): calcular las direcciones efectivas de cada operando de origen. Esto puede involucrar desplazamiento, indirecto de registro, indirecto u otras formas de direccionamiento. • Obtención de Operandos (FO, Fetch Operands): obtener cada operando desde la memoria. No es necesario para operandos que referencían a registros. • Ejecución de Instrucción (EI, Execute Instruction): realizar la operación indicada. • Escribir el Operando (WO, Write Operand): guardar el resultado en memoria. Con esta decomposición, las varias etapas tendrán una duración un poco más similar. Con fines ilustrativos, asumamos duración igual. Usando esta asumpción la figura siguiente muestra que la segmentación de 6 etapas puede reducir el tiempo de ejecución de 9 instrucciones de 54 unidades de tiempo a 14 unidades de tiempo. Varias indicaciones son necesarias: el diagrama asume que todas las instrucciones pasan por las 6 etapas. Esto no es siempre cierto. Por ejemplo, una instrucción load no necesita la etapa WO. Sin embargo, para simplificar el hardware, se establece el timing de manera que cada instrucción requiere de todas las etapas. Además, el diagrama asume que todas las etapas pueden ser ejecutadas en paralelo. En particular, se asume que no existen conflictos de memoria. Por ejemplo, las etapas FI, FO y WO involucran acceso a memoria. El diagrama implica que todos estos accesos pueden ocurrir simultáneamente. La mayoría de sistemas de memoria no permiten eso.
Otros factores limitan las mejoras. Si las seis etapas no tienen duración igual, algunas etapas deberán tener periodos de inactividad como se discutió para la segmentación de 2 etapas. Otra dificultad son las instrucciones de bifurcación, que pueden invalidar varias obtenciones de instrucción. La figura siguiente muestra los efectos de un salto condicional usando el mismo programa que la figura previa. Asuma que la instrucción 3 es un salto condicional a la instrucción 15. Hasta que la instrucción se ejecuta, no hay manera de saber qué instrucción se necesita obtener en seguida (puede ser la 4 o la 15). En este ejemplo, se precarga la instrucción 4 y se procede secuencialmente. En la figura de arriba el salto no se toma y se obtienen los beneficios de desempeño del esquema de segmentación. Sin embargo en la figura de abajo, se efectúa el salto aunque no ésto no se determina hasta la unidad de tiempo 7. En este punto, las instrucciones que se encuentren en el sistema deben ser descartadas. Durante la unidad de tiempo 8, se carga la instrucción 15. No se completa nunguna instrucción durante las unidades de tiempo 9 a 12, éste es el precio que hay que pagar por no haber predecido corréctamente el salto.
Otros problemas aparecen que no ocurrían en la organización simple de dos etapas. La etapa CO puede depender de los contenidos de un registro que puede ser alterado por una instrucción previa que puede aún no haber terminado su ejecución.
De la discución anterior, podría parecer que mientras mayor sea el número de etapas, mayor el ritmo de ejecución. Algunos diseños prácticos mostraron dos factores que parecen frustrar este patrón para diseño de alto rendimiento: 1. Para cada etapa, existe cierta sobrecarga para mover los datos de buffer a buffer y al realizar funciones de preparación y de entrega entre etapas. Esta sobrecarga puede alargar de manera apreciable el tiempo de ejecución total de una sola instrucción. Ésto es particularmente significativo cuando instrucciones secuenciales son lógicamente dependientes. 2. La cantidad de lógica de control requerida para manejar las dependencias de memoria y registros y para optimizar el uso de la segmentación aumenta enormemente con el número de etapas. Ésto puede llevar a una situación en la cual la lógica que controla el proceso entre etapas es más compleja que las etapas que se quieren controlar. Lidiar con los saltos Como se vió, uno de los mayores problemas al diseñar la segmentación de instrucciones es asegurar un flujo estable de instrucciones para las etapas iniciales. El empedimento principal son las instrucciones de salto condicional. Hasta que la instrucción se ejecuta, es imposible determinar si se hará el salto o no. Una variedad de enfoques han sido utilizados para lidiar con saltos condicionales: • Flujos múltiples • Preobtención de instrucción objetivo • Buffer de ciclo • Predicción de salto • Saltro retrasado FLUJOS MÚLTIPLES. Un flujo simple de segmentación sufre el costo de una instrucción de salto porque debe elegir una de entre dos instrucciones para pre-obtener y puede equivocarse en la decisión. Una solución de fuerza bruta es replicar las porciones iniciales del flujo y crear un nuevo flujo de segmentación de manera que cada flujo precargue una de las dos instrucciones y, de esta manera, no hay manera de fallar. Existen dos problemas con este enfoque: • Con flujos múltiples existen retrasos por la pelea que hay por el acceso a los registros y a memoria. • Instrucciones de salto condicional adicionales podrían entrar al flujo antes de que se tome la decisión de salto original. Cada instrucción adicional requerirá de un flujo adicional. PREOBTENCIÓN DE INSTRUCCIÓN OBJETIVO. Cuando se reconoce una instrucción de salto condicional, se preobtiene la instrucción objetivo del salto (en el ejemplo previo, la instrucción 15) además de la instrucción secuencialmente inmediata (en el ejemplo, la instrucción 4). De manera que si el salto se toma, entonces la instrucción ya ha sido obtenida. BUFFER DE CICLO. Un buffer de ciclo es una memoria pequeña y de muy alta velocidad mantenida por la etapa de obtención. Contiene las N instrucciones obtenidas maś recientemente, en secuencia. Si se va a efectuar un salto, el hardware primero revisa si la instrucción objetivo se encuentra en el buffer. De ser así, la siguiente instrucción es obtenida del buffer. El buffer de ciclo tiene 3 beneficios: 1. Con el uso de la preobtención, el buffer de ciclo contendrá algunas instrucciones secuencialmente adelante de la instrucción actual. Así, las instrucciones obtenidas en secuencia estarán disponibles sin el tiempo de acceso a memoria usuales. 2. Si un salto ocurre hacia un objetivo que se encuentra apenas unas cuantas localidades adelante de la instrucción de salto, el objetivo ya se encontrará en el buffer. Esto es útil para la ocurrencia, bastante común, de secuencias IF-THEN o IF-THEN-ELSE. 3. Esta estrategia funciona particularmente bien para lidiar con ciclos o iteraciones, de ahí el nombre buffer de ciclo. Si el buffer es lo suficientemente grande para contener todas las instrucciones de un ciclo, entonces dichas instrucciones sólo necesitan ser obtenidas desde la memoria una vez, para la primera iteración. Para las iteraciones subsecuentes, todas las instrucciones necesarias se encontrarán en el buffer.
El buffer de ciclo tiene un principio similar a la memoria caché, pero dedicado hacia las instrucciones. Las diferencias son que el buffer de ciclo sólo retiene instrucciones en secuencia y es mucho más pequeño en tamaño que la memoria caché. PREDICCIÓN DE SALTO. Varias técnicas pueden ser utilizadas para predecir si un salto será tomado o no. Entre las más comunes están las siguientes: • Predecir que nunca se toma • Predecir que siempre se toma • Predecir por opcode • Bit de historia • Tabla de historia de saltos Las primeras tres estrategias son estáticas: no dependen de la historia de ejecución de la instrucción. Los últimos dos enfoques son dinámicos puesto que dependen de las ejecuciones previas. Los primeros dos enfoques son los más simples. El primero asume que el salto no se toma y continúa con la obtención de instrucciones en secuencia. El segundo asume que el salto siempre se toma y continuará obteniendo instrucciones posteriores a la instrucción objetivo del salto. La estrategia de predecir que nunca se toma es la más popular de todas las estrategias de predicción de salto. Estudos que han analizado el comportamiento de los programas han mostrado que los saltos condicionales se toman más del 50% del tiempo. Si el costo de preobtener de ambos caminos es el mismo, entonces siempre preobtener de la dirección objetivo debería tener un mejor desempeño que el camino secuencial. Sin embargo, en una máquina con paginación (memoria virtual), al preobtener de la dirección objetivo del salto es más probable ocasionar un fallo de paginación. La última estrategia estática consiste en tomar una decisión basándose en el opcode de la instrucción. El procesador asume que el salto se tomará para ciertos opcodes de bifurcación y que no se tomará para otros. Este enfoque ha reportado grados de éxito de hasta 75%. Las estrategias dinámicas intentan mejorar la precisión de las predicciones tomando en cuenta la historia de las ejecuciones previas de las instrucciones de salto. Por ejemplo, uno o más bits se pueden asociar a cada instrucción de salto condicional de manera que reflejen su historia de ejecución. Estos bits son tomados en cuenta por el procesador para tomar la decisión de qué instrucción preobtener cuando se encuentre una instrucción de salto la próxima vez. Los bits de historia no se almacenan en memoria, en lugar de eso, se mantienen en almacenamiento temporal de muy alta velocidad. Con un solo bit, lo único que se puede guardar es si en la última ejecución se tomó el salto o no. Un problema de utilizar un solo bit aparece en el caso de una instrucción de salto que casi siempre se toma, por ejemplo una instrucción de ciclo. Con sólo un bit de historia, un error en la predicción ocurrirá dos veces por cada uso del ciclo: una vez entrando al ciclo y otra al salir. Si se utilizan dos bits, se pueden utilizar para almacenar el resultado de las últimas dos ejecuciones de la instrucción asociada. La figura a la derecha muestra un autóma determinista que ejemplifica la toma de decisiones. Asuma que se comienza en el estado de arriba a la izquierda. Si se hace una predicción correcta, se mantiene el estado; si se hace una predicción errónea se cambia al estado de arriba a la derecha pero la predicción se mantiene. Únicamente si se toman dos decisiones erróneas de forma consecutiva se cambiará la decisión de tomar/no tomar el salto. El uso de bits de historia tiene un problema: si se toma la decisión de tomar el salto, la instrucción objetivo no puede ser obtenida hasta que su dirección, la cual es un operando de la instrucción de salto condicional, se ha decodificado. Se podría obtener mayor eficiencia si la obtención de la instrucción pudiera ser iniciada tan pronto como se toma
la decisión sobre el salto. Para este propósito, se debe almacenar más información en lo que se conoce como una tabla de historia de saltos. La tabla de historia de saltos es una pequeña memoria caché asociada con la etapa de obtención. Cada entrada en la tabla consiste de tres elementos: la dirección de la instrucción de salto, algunos bits de historia que registran el estado de uso de dicha instrucción, e información sobre la instrucción objetivo. En la mayoría de las implementaciones, el tercer campo contiene la dirección de la instrucción objetivo. Otra posibilidad para el tercer campo es contener la propia instrucción objetivo. Las ventajas/desventajas son claras: si se guarda la dirección objetivo se tiene una tabla más pequeña; si se guarda la instrucción objetivo se tiene un tiempo de obtención menor. La figura siguiente contrasta el esquema de tabla con la estrategia de predecir que nunca se toma el salto. Con esta estrategia, la etapa de obtención siempre trae la siguiente dirección de forma secuencial. Si se toma el salto, entonces alguna lógica en el procesador detectará ésto e indica que la siguiente sea traída de la dirección objetivo (además de limpiar el flujo). La tabla de historia de salto se trata como un caché. Cada preobtención dispara una búsqueda en la tabla. Si no se encuentra una coincidencia, la siguiente dirección secuencial se utiliza para la obtención. Si se encuentra una coincidencia, se hace una predicción basada en el estado de la instrucción.
Cuando se ejecuta la instrucción de salto, la etapa de ejecución actualiza la tabla de historia con el resultado. Si la predicción fue incorrecta, la lógica de selección se redirige a la dirección correcta para la siguiente obtención. Cuando se encuentra una instrucción de salto condicional que no está en la tabla, se incluye en la misma y se descarta una de las entradas existentes usando alguno de los algoritmos de reemplazo del caché.