Arquitectura de Computadoras Algebra de Boole: permiten estudiar sistematicamente técnicas digitales y algebras de conjunto. Esto nos facilita: 1) 2) 3) 4)
Formalización algebraica de requerimientos. Análisis y síntesis de los circuitos combinacionales. Minimización de números de componentes y dispositivos(eliminar redundancias). Auxiliar en análisis y síntesis de circuitos secuenciales.
Circuitos Integrados de Media(MSI) y Gran Escala(LSI) Ciertas combinaciones lógicas se encuentran disponibles como productos estandar que conviene adquirir en lugar de las compuertas discretas(AND,OR,NOT,NAND,NOR). Por esta razón las técnicas de diseño de funciones lógicas, con dichas compuertas, debe ser tomado como último recurso a tener en cuenta, cuando altos niveles de integración no pueden ser usados. Si definimos funciones lógicas sin considerar los circuitos MSI disponibles, podríamos terminar escribiendo ecuaciones que deberán ser mecanizadas por compuertas discretas, puesto que no hay disponibles los circuitos MSI adecuados. Multiplexor(Selector). Es un dispositivo que permite seleccionar una entre varias entradas de acuerdo al valor que asume un conjunto de líneas de dirección llamadas selector. La cantidad de entradas del multiplexor depende del número de selectores. Si se tienen n selectores se van a tener 2^n entradas. Basicamente la función lógica que resuelve un multiplexor(de 4 entradas) es: Y = D0*A’B’+ D1*A’B+D2*AB’+D3*AB Si esta ecuación se quiere mecanizar con compuertas, se necesitarían como mínimo 10 compuertas(4 AND, 1 OR, 5 NOT. Mirando la ecuación uno podría reconocer si el circuito resultante se adecúa a la función del multiplexor. Otra utilidad del multiplexor es generar y simplificar funciones lógicas. Remueve tantas variables como líneas de selección tenga, dejando para implementar mediante compuertas las funciones “residuo” de las restantes variables. Por ejemplo en un MUX de 4 entradas se pueden remover 2 variables, en un MUX de 8 entradas se pueden remover 3 variables, y en un MUX de 16 entradas se pueden eliminar 4 variables. Si tenemos una función de 4 variables y eliminamos 3 de ellas con un MUX de 8 entradas, nos quedan 8 funciones de una variable para conectar en la entrada del MUX. Por ejemplo si dicha variable es C, entonces las funciones posibles son: C, C’,0, 1. Como estas son todas las disponibles sin compuertas adicionales, podemos generar cualquier función de 4 variables con solo un MUX de 8 entradas. Nota: Strobe es una entrada adicional y opcional que habilita o nó al selector, esto es, permite independizar entradas y salidas. Para funciones simples puede ser conveniente usar compuertas básicas. El MUX es indicado solo para las funciones más dificultosas. Además, términos producto comunes no pueden ser compartidos entre dos funciones usando MUX(en este caso podría ser útil usar compuertas básicas). En general la elección del método puede ser hecha teniendo en cuenta el approach con compuertas y recordando que funciones pueden ser hechas mediante un único MUX de acuerdo a la siguiente tabla: Función Cualquiera de las 4 funciones de las 2 mismas variables Cualquiera de las 2 funciones de las 3 mismas variables Cualquier función de 4 variables Cualquier función de 5 variables
Tipo de Multiplexor cuadruple de 2 entradas doble de 4 entradas 8 entradas 16 entradas
1
Podemos minimizar la generación de las funciones residuo eligiendo las variables que van a la entradas de dirección del multiplexor de manera tal que las funciones residuo serán las triviales o las que aparecen repetidamente. Ya que las funciones residuo pueden ser usadas en la generación de muchas funciones diferentes, trataremos de usar las variables que semejan aparecer en muchas combinaciones diferentes como direcciones del multiplexor.(usando diagramas de Veitch) Como simplificar funciones de muchas variables: como para más de cuatro variables el diagrama de Veitch se hace inmanejable, el uso de multiplexores reduce en mucho el tamaño del problema. Por ejemplo usando un MUX de 16 entradas para “remover”cuatro variables de una función de ocho variables es más facil de manejar. Además de hacer la solución del problema mucho más facil, se reduce el número de IC requeridos para mecanizar la función. Por supuesto, los multiplexores tambien pueden ser usados para generar funciones residuos. El máximo número de funciones residuos que se pueden generar con dos variables son 16: A, A’, B, B’, 1 y 0. Además AB, A’B, AB’, A’B’, (A’B)+(AB’) {XOR}, más sus complementos. Arboles de multiplexores: debido a que en la implementación de multiplexores estamos limitados en el número de pins o entradas al integrado, si queremos construir multiplexores de más de ocho entradas, podemos hacer cualquier tamaño de multiplexor simplemente interconectando varios multiplexores en una estructura de árbol. Esta característica de contruir grandes multiplexores extiende nuestra capacidad de generar funciones lógicas con multiplexores. Por ejemplo con un multiplexor de 64 entradas(usando 9 MUX de 8 entradas) puedo generar funciones lógicas de 7 variables.
Decoders/Demultiplexores. El opuesto a un multiplexor es un demultiplexor, o decoder. En un decoder una dirección de entrada binaria determina cúal de varias salidas será baja. Si tienen 4 salidas deben tener 2 bits de entrada, si tienen 8 salidas, entonces tendrán 3 bits de entrada. Un ejemplo típico es el decoder BCD(de 4 a 16), que decodifica un número de binario a decimal. Las salidas del decoder 10-16 son opcionales. El decoder tambien puede usarse para implementar funciones lógicas. En realidad las salidas son miniterms. El decoder es un generador de miniterms. Usar multiplexores o un decoder para manejar los bits de un determinado modo depende en gran medida de cuantas funciones hay. Se requiere un multiplexor para cada función. De manera que se prefiere un multiplexor cuando no hay muchas funciones. Las salidas de un único decoder, sin embargo, pueden ser usadas para la generación de un gran número de funciones, de manera que es más es eficiente donde hay un gran número de funciones. Hay algunos casos donde los dos tipos de circuitos se pueden usar juntos: los multiplexores son usados para eliminar variables y los decoders son usados para generar funciones residuales más complejas(el decoder puede generar miniterms para funciones residuos de un multiplexor). Se puede usar el mismo principio con demultiplexores para construir arboles y poder generar muchas funciones con solo ir agregando niveles al árbol de demultiplexores.
Encoders. Solo se activa una de las variables a codificar por vez. Priority encoder: se puede activar más de una entrada pero codifica la que tiene mayor prioridad.
1
1 1
0 1 2 3 4 5 6 7
1 1
codifica el 6
0
2
ROM(Read Only Memory). Las memorias ROM representan una estandarización de la implementación de funciones lógicas. Actúa como la memoria RAM pero con la característica de ser de lectura solamente. Es más simple que la RAM pero es no volátil(si uno corta la energía de la máquina no se pierde la información). La ROM se debe fabricar en cantidades suficientes que justifiquen su existencia. El uso de ROM permite implementar, mediante un procedimiento estandar que provee el fabricante, circuitos lógicos que producen una dada función. Tambien puede utilizarse como conversor de código. Implementación de funciones lógicas con ROM: Las líneas de dirección de la ROM corresponden a las variables de la función. En cada celda se debe poner el valor que toma la función con cada combinación(0 o 1). Cada celda de la ROM sería un miniterm. Puesto que la ROM puede mecanizar cualquier función, representan una potenciabilidad excesiva; por lo tanto se desperdician recursos. Cuando usamos una ROM para implementar una función, esta debe ser necesariamente expandida. Cuando expandimos un termino que carece de n variables generamos 2^n terminos para la función. Para hacer un uso completo de las capacidades de una ROM las variables de entrada y salida tiene que ser eficientemente codificadas de manera tal que cada variable tenga un contenido de información alto. Cuando más compactamente sean codificadas las entradas y salidas de una ROM más complicadas serán las ecuaciones lógicas. Sin embargo, ya que el tamaño de la ROM está determinado solamente por el número de entradas y salidas el resultado es un ahorro en HW. Podemos mirar a las entradas como grupos que pueden ser externamente codificados y a las salidas como grupos que pueden ser externamente decodificados. Cualquier tipo de variables que es mutuamente excluyente(esto es, solamente uno es verdadero por vez) puede ser codificado. El tamaño de la ROM depende de: Cantidad de locaciones(2^numero de variables) Tamaño de cada locación. Número de funciones. Por cada entrada que bajamos la ROM va a la mitad. El tamaño de la ROM es directamente proporcional al número de salidas. Los multiplexores son a veces útiles para reducir el número de entradas de una ROM. Un encoder puede ser útil para minimizar la cantidad de entradas de una ROM. Resumiendo: Desventajas: para más de 3 variables se torna un HW excesivo. Es imposible hacer cambios. Ventajas: permite fabricar circuitos en un proceso estandar, o sea no hago diseños en particular para cada cliente. Además llevado a un circuito integrado favorece el proceso de integración. PROM(Programmable Read-Only Memory). Tiene la misma funcionalidad que la ROM pero no tiene el problema de la escala. El fabricante entrega un circuito que, mediante un PROM-Programmer, permite programar la ROM; pero una vez hecho no lo puedo modificar. En cambio la EPROM permite mayor flexibilidad ya que es posible borrar y volver a programar. Utilidad: sistemas experimentales de grandes volumenes pueden probarse mediante PROMs hasta el momento en que el sistema esté listo para ser estandarizado mediante ROMs. PLA(Programmable Logic Arrays). El arreglo lógico programable(PLA) es una solución al problema de las entradas mencionado para la ROM. Con un PLA especificamos no solamente un patrón de bits de salida para cada palabra sino que tambien se especifica la dirección de la palabra. Esto hace posible tener más entradas para el mismo tamaño de ROM. Por ejemplo, un PLA de 96 palabras x 8 bits= 768 bits puede tener 14 entradas. Si todas las combinaciones de entrada definen una palabra(como es el caso de la ROM), entonces 2^14 = 16384 palabras o 16384 x 8 = 131072 bits de ROM. Con un PLA se puede definir 96 palabras en termino de 14 entradas. Puesto que se especifica 1, 0 o X sobre cada una de las
3
14 entradas, esencialmente se hace la compresión del código de entrada dentro de la porción de decodificación de palabra del chip. Entonces: _ si tengo n variables en una ROM => 2^n locaciones. _ si tengo n variables en un PLA << 2^n locaciones. Como con la ROM, podemos mirar al PLA como una memoria o como un generador de funciones lógicas o como un conversor de código. Si la pensamos como un generador de funciones lógicas, tenemos que es una estructura lógica AND/OR donde las direcciones de las palabras son decodificadas por compuertas AND, y cada salida es un OR lógico de cualquiera de las compuertas AND. Cada salida puede así ser definida como una suma de terminos producto. No hay ninguna ventaja en reducir la cantidad de terminos producto debajo del número provisto por el PLA que se usa(reducir el número de variables en cada termino es irrelevante). Sin embargo se podría reducir los terminos adicionales tomando encuenta: Si hay terminos producto identicos, solo dejar uno. La redundancia se resuelve automaticamente, no lo cableo. Examinar cada función, y si hay terminos que difieren solo en la inversión de una variable, combinarlos. Por ejemplo: ACF + ACF’= AC(F + F’) = AC. La redundancia se resuelve automaticamente, no lo cableo. El tamaño de un PLA está caracterizado por: El número de palabras. La cantidad de direcciones que puede acceder. La cantidad de funciones o salidas. Cuando se usa más de un PLA, se pueden implementar funciones lógicas aún más complejas. Conclusión: si por ejemplo podemos seleccionar una de ocho entradas, podríamos hacerlo con compuertas discretas, o ROM, o PLA; pero en realidad lo más adecuado sería un multiplexor. Para funciones lógicas simples y pequeñas funciones no estandar es preferible compuertas discretas.
Circuitos Secuenciales En los circuitos combinacionales, los estados de salida son funciones solamente de los estados de entrada presentes: para una combinación particular de entrada habrá siempre una salida o, en caso contrario nunca habrá una salida. En los circuitos secuenciales los estados de salida son funciones no solamente de los estados de entrada presentes, sino tambien del estado presente del circuito. Los estados del circuito, son a su vez funciones de los estados pasados de los circuitos, que a su vez son funciones de estados de entrada pasados. Los circuitos secuenciales deben tener una clase de memoria para poder “recordar” los estados pasados. X’s(inputs)
Z’s(outputs) Lógica Combinacional W’s(Memory inputs)
Y’s(Memory outputs/ Secondary inputs)
4
Elementos de Memoria
Las entradas de una circuito secuencial se denominan entradas primarias. Hay diferentes tipos de elementos de memoria. Las salidas de los elementos de memoria reflejan el estado de los elementos de memoria y actúan como entrada secundaria al circuito. Los elementos de memoria pueden tener una o más entradas y una única salida, o dos salidas complementarias. El estado de dichas entradas puede tener los valores 0 o 1 y son llamadas excitaciones de memoria y los estados de salida resultante son llamadas respuestas de memoria. Una propiedad de los elementos de memoria es que hay un tiempo de demora entre un cambio en su excitación y la respuesta resultante. Este tiempo se llama tiempo de transición del elemento de memoria. Otra propiedad de los elementos de memoria es que pueden permanecer en un estado luego de que la excitación que provocó ese estado no está más presente. Operaciones por pulso y por nivel. Hay varios tipos de operaciones de un circuito secuencial que pueden ser resueltas en dos clases: operación por pulso y operación por nivel. La distinción entre operación por pulso y por nivel involucra la duración de la excitación de un elemento de memoria en relación con el tiempo de transición del elemento de memoria. Más especificamente en una operación por pulso, la excitación de memoria se va antes de que el elemento de memoria responda. En una operación por nivel, la excitación de la memoria está aún presente cuando el elemento de memoria responde. Nota: el tipo de operación está determinado por el tiempo de las entradas del elemento de memoria, no por las entradas del circuito. Excitaciones de elementos de memoria
W
Respuesta de elementos de memoria
Y
Tiempo de transición
Operación por pulso
Operación por nivel
Un pulso puede definirse como una señal de corta duración, un nivel como una señal de larga duración. Con referencia al modelo de circuito secuencial general, las señales x pueden ser pulsos o niveles; las señales y desde las salidas del elemento de memoria, son siempre niveles. Ya que las W y Z son en general funciones de las x y las y, las señales Z y W pueden ser pulsos o niveles; sin embargo para que una señal W o Z sea un pulso al menos una señal x debe ser un pulso. Note que las señales y son niveles ya sea que las señales W sean pulsos o niveles. En una operación por pulsos las señales W son pulsos; así, al menos una señal x debe ser un pulso. Las señales Z, sin embargo, pueden ser pulsos o niveles. En la operación por niveles todas las señales deben ser niveles. Restricciones sobre operaciones. En operaciones por pulsos se deben satisfacer los siguientes requerimientos: 1. Pulso mínimo: el pulso deberá ser lo suficientemente largo para causar que el elemento de memoria cambie de estado. 2. Pulso máximo: el pulso deberá irse antes que el nivel de salida del elemento de memoria pueda aparecer como una entrada del elemento de memoria. Este tiempo está compuesto por el tiempo de transición del elemento de memoria más los delays a través de la lógica combinacional. 3. Tiempo mínimo entre el comienzo de pulsos sucesivos: este tiempo deberá ser mayor que el tiempo de transición del elemento de memoria(el tiempo que el elemento de memoria necesita para estabilizarse internamente antes de que esté listo para aceptar otro pulso) y los delays a través de la lógica combinacional. La restricción 3 prohibe pulsos de entrada simultaneos. Teoricamente, se podrían permitir pulsos de entrada simultaneos si se puede garantizar que ellos arribaran juntos; practicamente esto no puede lograrse, y una operación confiable debe prohibir pulsos sobre más de una linea de entrada por vez.
5
Esta restricción se aplica a pulsos de entrada simultaneos pero no necesariamente a señales de entrada simultaneas; si algunas de las señales de entrada son pulsos y otras son niveles, los pulsos de entrada pueden ocurrir solo uno a la vez, pero los niveles de entrada pueden ocurrir en todas las combinaciones posibles. Cada combinación de niveles de entrada deberá ser ANDed con un pulso de entrada. Los niveles de entrada no cambiaran mientras los pulsos de entrada esten presentes
Nivel de entrada Pulso de entrada Los pulsos de entrada que son ANDed con niveles de entrada son comunmente llamados pulsos de reloj, y la correspondientes entradas en si mismas se llaman reloj. Generalmente las entradas por nivel son las entradas lógicas, mientras que los relojes se usan para controlar el tiempo que las entradas por nivel son usadas(sampled?). usualmente un reloj produce una secuencia de pulsos que son periódicos, pero esto no es un requerimiento.
En operaciones por niveles se deben satisfacer los siguientes requerimientos: 1. Mínimo tiempo entre cambios en un nivel de entrada: un cambio en el nivel de entrada causaría cambios en los estados de los elementos de memoria. Esto es, no pueden ocurrir cambios en un nivel de entrada hasta que el circuito esté totalmente estable. Al igual que operación por pulso, no puede garantizarse que cambios simultaneos en la entrada por nivel ocurran al mismo tiempo. Operaciones por pulso con reloj son tambien llamadas operaciones sincrónicas y las operaciones por nivel, asincrónicas.
Elementos de Memoria. En camino a tener un circuito que almacene información indefinidamente, será necesario introducir realimentación de salida a la entrada del circuito(parte de la salida se toma como entrada). Estos circuitos se conocen como FlipFlop(FF). Así como tenemos una compuerta lógica como el elemento combinacional básico y la ROM como un arreglo combinacional grande, tenemos los flip-flop como elemento de memoria básico(básicamente un bit RAM) y una RAM como un arreglo de memoria grande. Esencialmente compuertas y ROM son análogas a los flip-flop y RAM, pero con flip-flop y RAMs tenemos que agregar la dimensión del tiempo. Ambos dispositivos tienen una entrada reloj, y sus estados cambian solo después de una transición del reloj. Tipos de Flip-Fop. Flip-Flop D(delay): la ecuación nos dice que la entrada (D) es “guardada” en el flip-flop cuando ocurre un reloj y aparecerá en la salida(Q) durante el tiempo de reloj (n+1). El flip-flop D es más que un simple bit de RAM. Es muy útil para almacenamiento de datos y otras aplicaciones. La ecuación característica es: Qn+1 = Dn. Flip-Flop T(toggle): simplemente queda en su estado previo si la entrada T es falsa antes de la señal de reloj. Si la entrada T es verdadera, la salida cambia al estado opuesto cuando ocurre la señal de reloj. Este flip-flop es útil por ejemplo para contadores binarios donde queremos que cada bit se invierta en caso de que ocurra un carry en los bits de orden más bajo. La ecuación característica es: Qn+1 = (T’.Q + T.Q’)n Cualquier tipo de flip-flop puede ser construido a partir de un flip-flop D simplemente sumando algunas compuertas lógicas adicionales. Flip-Flop S-R(Set-Reset): setea si la entrada S es verdadera, resetea si la entrada R es verdadera. Queda indefinido si R y S son verdaderos. El estado de reposo está dado por las dos entradas (Set-Reset) en 0. La ecuación característica es: Qn+1= (S+R’.S’.Q) con R.S 1.
6
Es el FF más elemental a nivel lógico. Una vez que la excitación desaparece el circuito conserva su estado. Estando el circuito en S, la única forma de cambiarlo a R es poniendo un 1 en R; no interesa los valores dados a S. La inversa tambien vale. Este FF es asincrónico, responde en todo momento. Si queremos que el Set-Reset trabaje en forma sincrónica lo podemos implementar con un reloj y se denomina LATCH. El reloj inactivo(en 0) significa que el circuito está en un estado de no excitación y retiene información. Con el reloj activo el circuito puede ser excitado(seteado o reseteado). Una vez que se deshabilita el reloj el circuito ignora las entradas. Master-Slave: vimos que el LATCH es un dispositivo por niveles, por lo que no podemos utilizarlo para los sistemas secuenciales por pulsos. No permite ser leido y escrito simultaneamente. Esto da lugar a una estructura donde hay dos FF que se excitan en forma disjunta; uno es el director y el otro subordinado. Esta estructura se denomina MASTER-SLAVE(Director-Subordinado). El que realmente es excitado es el primer FF, Director, cuya función es dirigir al subordinado; esto es , hacer la evolución de la excitación y el subordinado se encarga de guiar la salida. Esquematicamente: Director
Subordinado
LATCH
LATCH
Director
Subordinado
LATCH
LATCH
t1...t4
> t4
En el momento que el director toma la decisión el subordinado está desconectado. Si el director está desconectado(reloj en 0) llegó un 1 al subordinado y se conecta. Flip-Flop J-K: setea si J es verdadero y resetea si K es verdadero. Es similar al FF S-R, excepto que si J y K son ambos verdaderas, la salida cambia al estado opuesto. La ecuación característica es: Qn+1 = (J.Q’+K’.Q)n Generalmente se trabaja con FF J-K para pequeños contadores y control o FF D para almacenamiento de datos. Usualmente los FF tiene dos entradas adicionales asincrónicas, PRESET y CLEAR, que permite el seteo o reseteo inmediato sin esperar por el reloj(son asincrónicos). Se utilizan para ciertos trabajos especiales, como setear condiciones iniciales. Registros y contadores. Los FF son frecuentemente agrupados como registros o contadores. Un registro se puede pensar como un almacenamiento de datos, y a menudo el contenido del registro tiene alguna interpretación física, tal como un código de carácter o un número. El registro en si mismo no tiene lógica (no hay procesamiento) y consiste simplemente de un conjunto de FF. Lógica combinacional es usada frecuentemente a la entrada de un registro para seleccionar el origen de los datos a ser mantenidos en el registro o para hacer conversión de código o lógica aritmética. El registro en si no realiza ninguna función lógica. Una forma de implementar los registros es a partir de FF D. Estos FF comparten el reloj y al activarlo obtengo información de todos. En general los FF controlados por reloj tienen dos entradas adicionales, que actúan en forma asincrónica, y que son CLEAR(pone a 0 el FF) y PRESET(pone a 1 el FF). Estas dos entradas fijan las condiciones iniciales antes de comenzar la evolución del mismo. Registros de Desplazamiento: hay registros que permiten desplazar información a derecha e izquierda a través de los distintos FF. Cada vez que se produce un pulso de reloj, cada uno de los FF toma la información del FF anterior o posterior. 7
Para poder tener un registro de este tipo, los FF deben poder leerse y escribirse simultaneamente, es decir deben ser del tipo MASTER-SLAVE.(tambien podrían recibir información en serie.ventajas??) Para almacenar información y adquirir datos bastaría un FF LATCH. Normalmente estos registros son un conjunto de FF organizados en un circuito integrado, donde se permite carga en paralelo, lectura en paralelo y corrimientos de izquierda a derecha o de derecha a izquierda. Aplicaciones de los registros de desplazamiento: Muchas veces es necesario convertir datos de serie a paralelo, por ejemplo, en la transmición de información desde la terminal a través del bus. Al nivel de la interfase, el bus recibe la entrada en serie y la entrega a la CPU en paralelo. La inversa de CPU a terminal es una conversión de paralelo a serie. Otra aplicación se da en las operaciones aritméticas donde utilizamos los corrimientos para multiplicar y dividir.
Los FF tambien pueden ser agrupados como contadores. Los contadores son usados para controlar secuencias de operaciones. Las salidas de los FF que hacen al contador se dicen que son el estado del contador. Un contador es un circuito secuencial diseñado para ciclar a través de K estados distintos, S0,......,Sk-1 en respuestas a pulsos de entrada(reloj). Las combinaciones de las salidas de los distintos FF constituyen los distintos estados del contador. El valor K se conoce como el módulo del contador. Si se tiene un contador de n FF, el modulo máximo es 2n. Normalmente(no exclusivamente) estos K bits van a representar K números consecutivos. Donde un estado Si+1 va a estar determinado por Si+1 modulo K La secuencia de estados puede ser sencilla, tal como contar cierto número de pulsos de entrada, o repeticiones de alguna operación,o pueden ser más complejas. Secuencias complejas son a menudo controladas por condiciones de entrada variables. En ambos casos un contador puede ser definido mediante el simple desarrollo de lógica combinacional para cada entrada del FF. El próximo estado(n+1) de FF está determinado por sus entradas antes del reloj(n). Diseño de contadores con FF. Se divide en dos pasos: 1. Definir los contadores requeridos en el sistema y una secuencia deseada de estados 2. Diseñar lógica combinacional para las entradas de los FF para producir esa secuencia de estados Los contadores se pueden clasificar en: Sincrónicos: todos los FF que constituyen el contador se excitan a partir de un mismo reloj. Asincrónicos: con el reloj se excita el primer FF y las señales de reloj de los demás FF están dadas por los estados internos de los mismos FF A su vez ambos tipos de contadores se pueden clasificar en: UP-COUNTER o DOWN-COUNTER. El contador asincrónico es más fácil de implementar que el sincrónico, ya que no se requiere de lógica adicional(simplicidad). La desventaja es la velocidad máxima de operación. Esto se debe a que los FF no son excitados al mismo tiempo, por lo que producen retardos por el tiempo de propagación. Cuanto mayor sea el número de FF que componen el contador mayor será el número de retardos entre la excitación FF y el reloj. Si el reloj fuera común a todos los FF, se tendría que esperar un solo retardo. El contador sincrónico para su implementación hace intervenir funciones lógicas a las entradas. Este tipo de contador es mucho más flexible para controlar el módulo que uno asincrónico. El diseño es igual al de un circuito secuencial, debemos preveer la evolución del circuito y en función de esto, la excitación.
Registros de desplazamiento como contadores: condicionando la información que entra al primer FF de la cadena de un registro de desplazamiento en función de los estados de los FF, es posible obtener una secuencia de estados distintos, a medida que la información se va desplazando a través del registro en forma sincrónica. A esta disposición se la conoce como generadora de secuencia. 8
Contador en anillo(Ring Counter): con n FF obtenemos un contador de módulo n. La desventaja de este esquema es que se requiere de muchos FF. Se necesitan tantos FF como estados se quieran. La ventaja radica en que los distintos estados de los FF están caracterizados por su salida; no es necesaria ninguna decodificación, lo que implica una mayor velocidad. Contador Moebius: algunos contadores de secuencia resultan tener una lógica muy simple. Por ejemplo, podemos hacer un contador de 8 estados con la misma característica de cambiar un único bit por cada cambio de estado. El estado de cada FF es igual al estado previo del FF de la izquierda. El estado del FF más a la izquierda es igual al complemento del estado del FF más a la derecha. Usando el mismo principio podemos hacer un contador Moebius de cualquier tamaño. Para cualquier tamaño n de un contador Moebius se tienen 2*n estados diferentes, por este motivo contadores grandes son impracticos. La secuencia Moebius es a veces útil para contadores de control de estados. Otra ventaja es que cualquier estado o grupo de estados consecutivos pueden ser decodificados con una única compuerta de dos entradas. Para decodificar un grupo de estados simplemente miramos al último 1 del primer estado del grupo y el primer 0 del último estado del grupo. Definición de contadores de control de estados: a menudo un contador puede ser más complicado que contar una secuencia binaria. A menudo la secuencia es complicada y se ve más como un registro que como un contador ya que responde más a entradas externas que su propio estado. El primer paso en el diseño de tales circuitos secuenciales es hacer el diagrama de estados. Un diagrama de estados tiene un circulo representando cada estado. Dentro de cada circulo está escrito la identidad binaria del estado y el significado del estado en la operación del sistema. Desde un estado salen flechas a todos los posibles proximos estados. Funciones lógicas muestran las condiciones requeridas para cambiar de estado. Ejemplo:
Idle 000 Read
EOF
Read Tape 100 Full Send 110 End Send Last Character
111 SP Send Check Code
011 SP
Wait For Acknowledge
001 ACK Read Pulse 101 9
TIMOUT
Comenzando por el estado inicial, vemos que presionando el boton READ nos pone en el estado READ TAPE. Si se detecta un EOF significa que no hay más nada sobre la cinta y retornamos al estado inicial IDLE. Cuando la RAM está llena(FULL) de datos leidos de la cinta, pasamos al estado SEND(enviar) y corremos la información hasta que END indica que la RAM está vacía. Esto nos pone en el estado SEND LAST CHARACTER, el cual es necesario porque los datos son enviados, un bit a la vez, desde el registro que está cargado por la RAM. Cuando el contador de bit indica que el último bit ha sido enviado, el estado de stop bit(SP) del contador de bits nos pone en el estado SEND CHECK CODE. Cuando el último bit del check code ha sido enviado, SP nos lleva al estado WAIT FOR ACKNOWLEDGE. Si se recive un ACKnowledge entramos al estado READ PULSE por un tiempo de reloj para simular que se presiona el boton READ y causar que otro registro sea leido de la cinta en el estado READ TAPE. La secuencia se repite hasta que todos los registros de la cinta son enviados y el EOF retorna el sistema al estado IDLE. Si en el estado WAIT FOR ACKNOWLEDGE no se recibe un ACK dentro 1/3 seg., la señal TIMOUT causa que el registro sea enviado nuevamente. Asignación de estados: una vez que definimos los estados requeridos, asignamos códigos binarios a esos estados. Como estos códigos corresponden a los tres FF que hacen al contador, las asignaciones tendrán un fuerte efecto sobre la lógica requerida para mecanizar el contador de estados y la otra lógica en el sistema. La lógica en el resto del sistema puede ser minimizada haciendo que los FF del contador correspondan a las señales de control requeridas. Por ejemplo en la asignación de estados del ejemplo anterior, el FF del medio está seteado durante los tres estados en los cuales estamos habilitados para enviar datos(110, 111, y 011). Esto significa que podemos usar la salida del FF del medio directamente como “enable send” en lugar de tener que usar compuertas para generar una función que sea true durante estos tres estados. La lógica del contador en sí misma puede ser minimizada recordando que ciertas secuencias del contador son más faciles de generar que otras. Todos los cambios de estado(excepto el TIMOUT) del ejemplo requiere el cambio de estado de un solo FF. Esto significa que se necesitarán compuertas en la entrada de un solo FF. Para simplificar el contador, los primeros 6 estados están dados por la secuencia “moebius”. Tambien al hacer el estado inicial 000 es posible inicializar el contador mediante la entrada CLEAR del FF. Como podemos construir un contador asincrónico de cualquier longitud(poniendo contadores en cascada), la utilidad de contadores discretos con FF está limitada a secuencias especiales o a contadores muy pequeños(solo modulo 2, 3, 4). Aún contadores de control de estados a menudo pueden ser hechos con un contador MSI. Cualquier contador de control de estados puede pensarse como una secuencia lineal con “jumps” y “branchs”. Si asignamos los estados en una secuencia binaria, y usamos LOAD y CLEAR para hacer los branches podríamos usar circuitos integrados ya disponibles para la construcción del contador.
Algoritmos Aritméticos Sistemas numéricos posicionales: la gran mayoria de las computadoras digitales usan sitemas numéricos posicionales para representar información numérica. En este modo de representación, un número es codificado como un vector de digitos donde cada dígito tiene un peso de acuerdo a su posición. Asociado con el sistema numérico hay una base b, tal que el rango de cada dígito va de 0 b-1. Así un número x en base b se representa como sigue: X = (Xn, Xn-1,.......Xi,.......,X1, X0, X-1,........,X-m) Y su valor (considerando por ahora solo números positivos) denotado |X|: n
|X| =
xi bi i=-m
Dentro de las computadoras se adopta la base 2. La desventaja de tener que convertir de decimal a binario es compensada por una ganancia en la representación de almacenamiento y la fácil implementación de operaciones básicas.
10
Para facilitar la comunicación entre la representación binaria interna y alguna notación más cercana a nuestra forma de pensar, se pueden usar octal(b=8) y hexadecimal(b=16). Como los sistemas númericos tienen bases que son potencias de 2, las conversiones entre binario, octal y decimal son triviales. Repersentación de enteros: los tres sistemas numéricos posicionales que se usan son: Signo Magnitud. Complemento a 1. Complemento a 2. Los tres sistemas permiten un fácil testeo del signo mediante el chequeo de un solo bit. Signo Magnitud: en esta representación, un bit designado, en general el de más a la izquierda, indica el signo del entero(0 para +, 1 para -). El rango es [-(2n-1 – 1), 2n-1 – 1)]. Uno de los problemas es la representación dual para el 0(00......0, 10......0). Además hay problemas cuando se suman números de signos opuestos, porque debemos comparar sus magnitudes para conocer el signo del resultado. Complemento a 1: los enteros no negativos en el intervalo [0, 2n-1-1] son representados como en un sistema posicional binario. Para representar enteros negativos en el intervalo [-(2n-1 –1), 0] primero representamos sus valores absolutos y luego complementamos bit por bit, es decir reemplazamos los 0’s por 1’s y los 1’s por 0’s. Mas generalmente, para un sistema posicional en base b, reemplazamos cada dígito Xi por (b-1)- Xi. Como en signo magnitud, el reconocimiento del signo es trivial puesto que el bit más a la izquierda es 0 para los positivos y 1 para negativos. Pero tenemos una representación dual para el cero(00......0, 11.......1). pero la suma y resta son más faciles de implementar comparando con signo-magnitud. Complemento a 2: los números no negativos en el rango [0, 2n-1 – 1] se representan como en complemento a 1. Para un entero negativo cada bit es complementado y un 1 es sumado a la posición menos significativa. El test del signo se hace facilmente. Hay una única representación para el 0. Tienen rango similar, pero ligeramente desequilibrado(existe un negativo más). No es simetrico. Es ligeramente más simple que operar en complemento a 1.
Suma y resta. Suma en sistemas numéricos posicionales: dado un sistema numérico posicional en base b, la suma de dos números positivos de n-digitos, x e y: X = (Xn-1,.......,X1, X0),
Y = (Yn-1,.......,Y1, Y0)
Resulta en la suma S = (Sn, Sn-1,.......S1,S0) donde Sn puede tomar uno de dos valores 0 o 1 independientemente de b. Cuando Sn es 1, se considera a menudo como overflow. El algoritmo puede ser expresado como: 1. Co 0 (Co es el carry inicial); 2. For i := 0 step 1 until n-1 do Begin Si (Xi + Yi + Ci) mod b; Ci+1 (Xi + Yi + Ci) / b End; 3. Sn Cn; Como Xi + Yi 2(b-1) y C0 = 0, el valor máximo para cualquier Ci sería (2(b – 1) + 1) / b = 1. En este algoritmo cada dígito es examinado una vez. Por lo tanto el proceso es de una complejidad del O(n). Esto es, el algoritmo de suma corre en tiempo proporcional a n. Como todos los dígitos tienen que ser examinados, teoricamente no se puede esperar ninguna mejoría. Pero naturalmente consideraciones de implementación son importantes para realizar el HW. 11
Facilmente podemos diseñar un algoritmo para la resta, incorporando la generación de borrows en lugar de carries en cada dígito que se resta. Consideraciones de HW y los sistemas numéricos complemento hacen innecesaria la distinción entre suma y resta. Para el caso binario, podemos transladar el paso 2 del algoritmo en terminos de valores booleanos true(1) y false(0) como sigue: Si (Xi Yi) Ci Ci+1 Xi .Yi + (Xi + Yi) . Ci
(1)
Hay muchas implementaciones posibles para el sistema (1), dependiendo de la disponibilidad de compuertas(AND, OR, NOT, NAND, NOR, EOR). En general la implementación de un FULL ADDER de un bit se hace con dos HALF ADDER. Un HALF ADDER con dos entradas A y B realiza las dos salidas siguientes: SAB CA.B Circuito Logico A
Esquema AB
A
S
B
C
S B A.B C
Un HALF ADDER(semisumador) suma los dos operandos sin tener en cuenta el carry; esto es , resuelve solo una parte del circuito. El sistema (1) se puede construir entonces con dos HALF ADDER porque podemos escribir Ci+1 como: Ci+1 Xi .Yi + (Xi Yi) . Ci (xj yj) cj
cj Cin
S
A B
xj yj
xj yj A
S
B
c xj . yj
A
S
B
C
sj (xj yj) . cj
cj+1
Co
Esquema
Circuito Logico
Un solo FULL ADDER y un bit de almacenamiento, o Flip-Flop, para retener el potencial carry es suficiente para una implementación en HW del algoritmo de suma(en el caso de sumador serial). Si asumimos que las entradas y salidas están en registros de corrimiento o que existen circuitos de selección para obtener los n dígitos serialmente, entonces la suma podría hacerse exactamente como en el algoritmo. Sumadores seriales no son usados en el diseño de procesadores centrales o ALUs porque son demasiados lentos(los elementos de memoria implican retardo, por ejemplo) y voluminosos o restrictivos. Sin embargo pueden ser útiles en dispositivos seriales como memorias CCD(memorias de núcleo). Un esquema de un sumador serie seria:
12
Xn-1
x0
Ci A
S
Xn-1
x0
B
Ci+1
sn-1
s0
D Re Q
Aquí la suma se resuelve en n pulsos de reloj. Sumadores en Paralelo. Ripple Adder: en lugar de pasar todos los bits de un operando secuencialmente a un solo Full Adder, podemos presentar los n bits de los 2 operandos en paralelo a n Full Adders. Estos Full Adders están conectados de tal forma que el carry out del Full Adder i, Ci+1, es el carry in para el Full Adder i+1, para 1 i n-1. Una descripción esquemática para un sumador de 4 bits es la siguiente: X4 Y4
X3 Y3 C4
X2 Y2 C3
X1 Y1 C2
C1
C5 S4
S3
S2
S1
Como el carry puede propagarse desde C1 hasta las ultimas salidas de suma y carry, esto es propagarse a través de n full adders, el tiempo en el peor caso es n niveles de full adders. Un sumador sincrónico(todas las sumas = tiempo) entonces O(n). Un sumador asincrónico(ajustar el tiempo de la suma) entonces O(log n) . Se trabaja en forma sincrónica para tener en cuenta el peor caso. Carry Look Ahead Adders: si examinamos como el carry se propaga desde el full adder i hasta el full adder i+1, es decir si examinamos la ecuación: Ci+1 Xi .Yi + (Xi Yi) . Ci Vemos que la transmisión se puede descomponer en: Carry generado: Gi+1 Xi . Yi Carry propagado: Pi+1 Xi Yi Y Ci+1 Gi+1 + Pi+1 . Ci Comenzando con el carry-in inicial c1, or g1 = c1, tenemos la recurrencia:
13
i
Ci+1 Gi+1 + ( j=1
i+1
Pk ) . Gj
(3)
k=j+1
( y son los operadores OR y AND logicos). Por ejemplo para un sumador de 4 bits se tendría: C2 G2 + (P2.C1) C3 G3 + (P3.G2 + P3.P2.C1) C4 G4 + (P4.G3 + P4.P3.G2 + P4.P3.P2.C1) C5 G5 + (P5.G4 + P5.P4.G3 + P5.P4.P3.G2 + P5.P4.P3.P2.C1) (*) Los terminos entre paréntesis son los terminos que se propagan. El despliegue lógico de un sumador típico de este tipo es llamado Carry Look Ahead(CLA) debido a que el carry puede ser generado o predicho antes de que su correspondiente bit de suma sea obtenido. Ventajas: En vez de tener que esperar todas las etapas, genera el carry como una función independiente de la suma. Hay aproximadamente el mismo número de compuertas que en el Ripple Adder, pero el peor caso en tiempo es ahora de 4 niveles de compuertas en vez de 8 en el peor caso del Ripple Adder. Desventajas: Este proceso en paralelo no es viable para un n(bits) considerable, es decir, no se podría llevar a la práctica porque habría limitaciones de Fan In(compuertas de muchas entradas) y Fan Out(muchos terminos para alimentar). No se usa para más de cuatro bits. (*) las ecuaciones crecen muy rapidamente y aparecen limitaciones de Fan In y Fan Out.
De la misma manera que los Full Adders son unidos en cascada en un Ripple Adder, los CLA pueden ser conectados con el carry-out de uno con el carry-in del siguiente.
Un nivel de CLA Un box CLA es el equivalente a un CLA de 4 bit con el carry-out generado como en (3). Este concepto puede ser extendido a algunos niveles. Podemos agrupar los n bits en A grupos de b bits, y generar un término G y un término P para cada grupo. Por ejemplo, para el grupo “a” (bits(a-1)b + 1 hasta ab) tenemos: ab+1
Pa
i=(a-1)b+2
ab
pi
Ga
j=(a-1)b+1
El carry-out del grupo a, denotado Ca es entonces:
14
ab+1
( i=j+1
pi ) . gj + gab+1
(4)
a-1
a
Ca Ga + ( Pi ) . Gj j=1
(5)
i=j+1
y entonces el carry en la posición k(k = ab + 1 + m, 0 m b-1) es: k-1
Ck gk +
k
(
j=ab+2
i=j+1
k
) . gj +
j=ab+2
pi . Ca
(6)
Carry Look Ahead Generator(CLAG): antes implementamos las funciones en forma directa, en cambio ahora hacemos una etapa y luego otra, ganando en velocidad pero pagando el precio de tener mayor número de compuertas. Un esquema de un circuito para sumar palabras de 16 bits con CLA de 4 bits y que usa CLAG(circuito que genera los carries a partir de las funciones P y G): CLAG G5 P5
Cn+z
CLA
S16
S13
G4 P4
Cn+y
G3 P3
CLA
S12
Cn+x
G3 P3
CLA
S9
S8
CLA
S5
S4
S1
G5 y P5 se usan en caso de tener un nivel más para G y P. Con un tercer nivel podría manejar 64 bits con 4 secciones de 16 bits. Las variables z, x e y se usan porque puedo utilizar sumadores de distintas cantidades de bits. Carry Save Adder: si en vez de tener que sumar solamente dos operadores, necesitamos sumar m de ellos, m>2, como por ejemplo la multiplicación, podemos acelerar esta operación permitiendo que la propagación del carry se accione solamente durante la última suma. Otra vez usamos n full adders, pero en lugar de conectarlos como en el ripple adder, los dejamos desconectados y tomamos como entradas 3 de los m operandos, digamos x, y, z. las dos salidas generadas: SSi Xi Yi Zi CSi+1 Xi . Yi + (Xi Yi) . Zi
(7)
Junto con otra de las entradas originales pueden ser consideradas como entradas a otro(o el mismo) bloque realizando las operaciones del sistema (7). Este proceso se repite hasta que queden dos operandos o sumas parciales. La última suma requiere un ripple adder(o cualquier adder que implemente el sistema (1)). Ventajas: {preguntar!!!!!} Es una implementación sencilla con el HW mínimo para implementar full adders independientes. Es la condición ideal en cuanto a velocidad, porque cuando voy a operar tengo simultaneamente todos los operandos. Esquema:
15
Xn-1
SCn-1
Yn-1
Zn-1
SSn-1
SCn-1
Xn-2 Yn-2
Zn-2
SSn-1
X0
SC0
Y0
Z0
SS0
Multiplicación y División. El esquema de la multiplicación: dados dos enteros positivos de n-digitos, el multiplicando x y el multiplicador y, representados en un sistema numérico posicional de raiz b, decimos: X = (Xn-1,...........,X0) Y = (Yn-1,.............,Y0) Su producto p es un número positivo de 2n digitos: P = (P2n-1,........,P0) El cual puede ser obtenido por el siguiente algoritmo: 1. setear Pj 0, 0 j < 2n; 2. for i:= 0 step 1 until n-1 do 3. if Yi 0 then begin k 0; for j := 0 step 1 until n-1 do begin t Xj . Yi + Pi+j + k; (*en base b*) Pi+j t mod b; K t/b; End; Pi+n k; End; Este es el método usual donde la acumulación de dígitos se hace tan pronto como sea posible en lugar de dejar que los productos parciales sean desarrollados al final. A causa de los ciclos anidados, para i y j, es fácil ver que tenemos un proceso O(n2). Pero a diferencia de la suma, la multiplicación si se puede acelerar. La idea básica usada para acelerar la multiplicación es dividir multiplicador y multiplicando como sigue(asumimos x e y números de 2n-dígitos y su producto 4n dígitos, además asumimos sin perdida de generalidad, b=2): x = X 1 . 2n + X 0
y = Y1 . 2n + Y0
con X1 = (x2n-1,........,xn) y X0 = (xn-1,.......,x0), y Y1, Y0 tratados similarmente. El producto p es ahora: p (22n + 2n)X1 . Y1 + 2n(X1 – X0) . (Y0 – Y1) + (2n + 1)X0 . Y0
(8)
en orden de obtener p mediante la ecuación (8) reemplazamos una multiplicación de 2n-bit por 3 multiplicaciones de n-bit, o en terminos del tiempo: T(2n) 3T(n) + cn
(9)
Para alguna constante c relacionado con tiempos de adición y corrimiento(T(m) es el tiempo de hacer la multiplicación de 2 números de m-bit). 16
La solución a la ecuación (9) es : T(n) 3c(nlog3) Esto es, el proceso de multiplicación puede ser reducido de O(n2) a O(n1.59) Multiplicación de enteros binarios. Esquema básico para enteros signados: sean x=(xn,......x1), y=(yn,.......,y1) y p=(p2n,.......,p1) el multiplicando, multiplicador y producto. Usando un Ripple o un CLA con un registro de corrimiento, el esquema de multiplicación dado anteriormente puede ser levemente modificado e implementado como sigue para x e y positivos, dando un p positivo. 1. setear Pj 0, 1 j < 2n; 1. for i:= 1 step 1 until n do 2. if Yi 0 then begin Ci 0; for k := 0 step 1 until n-1 do begin Ci+k+1 Pi+k . Xk+1 + (Pi+k Xk+1) . Ci+k; Pi+k Pi+k Xk+1 Ci+k; End; Pi+n+1 Ci+n+1; ver !!!!!!!!! End; Para signo-magnitud, los (n-1) bits de magnitud se multiplican como dice el algoritmo anterior, P2n-1 = 0 y el signo del producto, P2n es: P2n Xn Yn. Para 1 y 2 complemento hay que tener más cuidado. Si x e y son los dos positivos no hay problemas. Sin embargo, si uno(o ambos) son negativos el algoritmo no dará resultados correctos. Una posible solución sería convertir a signo-magnitud, con el costo de dos(o tres) conversiones. Consideremos el caso de 2 complemento. Si x es negativo e y es positivo entonces: Si x = 2n - |x| y = |y| x.y = (2n-|x|).|y| = 2n |y| - |x.y| pero el resultado deseado, un número negativo, sería la representación en 2 complemento de x.y, i.e., 22n.|y|- |x.y|. La acción correctiva sería hacer una extensión de signo del multiplicando, esto es, representar a x como 22n - |x|. Ahora: x.y = (22n - |x|).|y| = 22n.|y| - |x.y| Una aparente crítica es que parece que se necesitara de un sumador de 2n-bits, puesto que x tiene el doble de su longitud original; sin embargo son suficientes un sumador de n bits y un registro de desplazamientos de 2n-bits. Sin embargo un sumador de n-bit y un registro de desplazamiento de 2n-bit son suficientes. La técnica que se aplica es la siguiente: El multiplicando(x) está en un registro de n-bit, el multiplicador y el producto comparten un registro de desplazamiento de 2n-bit. El multiplicador está en los dígitos menos significativos, y el producto, inicialmente 0, en el extremo más significativo. Cuando el bit menos significativo es 0, el registro de desplazamiento se corre a derecha una vez con el signo del multiplicando siendo insertado como el bit menos significativo. Cuando es 1, el multiplicando(x) es sumado a los n bits más significativos, el registro se corre a derecha una vez y sus n bits más significativos son reemplazados con los (n+1) bits resultantes de la suma(n bits) a la cual se concatena, a la
17
izquierda, el signo del multiplicando. Este procedimiento se repite para los n bits del multiplicador, resultando en la representación en 2 complemento deseada. Si x es positivo e y es negativo, entonces: Podriamos extender y de la misma manera. Pero esto sería hacer el doble de iteraciones. Causa overhead. Una mejor solución sería generar x.(2n – y) y entonces hacer una resta adicional 2n .x. esta resta es en efecto la suma (22n – 2n . x) y el resultado 22n - |x.y|. Si x e y son negativos, entonces: Expandimos x y restamos. La operación resultante tiene la forma: (22n – x).(2n – y) – (22n – 2n.x) = 23n – 22n.y + x.y – 22n con todo lo que sea mayor que 22n siendo descartado. Por lo tanto tenemos como resultado x.y, un número positivo. Recodificación del multiplicador: cuando el bit actual del multiplicador es 0, no se hacen sumas según el esquema de multiplicación y el producto parcial es simplemente corrido a derecha una posición(paso 3 del algoritmo). Esto puede generalizarse a corrimientos de longitud variable(saltos) si se detectan cadenas de 0’s. Además si el sumador es tambien un substractor eficiente,como por ejemplo en la representación 2 complemento, podemos hacer lo mismo con cadenas de 1’s. Para justificar esta operación, consideremos una cadena de 1’s en el multiplicador de la forma: ****011110** j i(i-1)
(bits del multiplicador) (posición de bits)
el proceso de multiplicación entre los bits Yj y Yi en el algoritmo original computa: x . (2i + 2i+1 + .........+ 2j-1) = x.(2j – 2i) Entonces las (j – i) sumas pueden reemplazarse por una suma y una resta. Se necesitará un flag para controlar si se suma, se resta o se hace un salto. Reescribimos el algoritmo como: 1. setear Pj 0, 1 j < 2n; flag 0; 2. for i:= 1 step 1 until n do if (Yi = 1 and flag = 0) or (Yi = 0 and flag =1) then 3. if Yi = 0 then begin p p+x; flag 0 end else begin p p-x; flag 1 end; Trabajando en complemento a 2 cuando y era negativo debiamos restar 2n . x. Cuando hacemos saltos esto queda automaticamente resuelto ya que la extensión de signo se hace agregando unos por la izquierda y este string de unos nunca termina, por lo tanto nunca alcanza a sumar 2n.x, luego no es necesario restarlo. El peor caso es que no haya saltos sobre 0’s cuando se ejecuta el algoritmo. Esto ocurre con multiplicadores de la forma 0101....0101. Otra alternativa para saltar sobre cadenas de 0’s y 1’s sería recodificar c bits del multiplicador a la vez y tener 2 c – 1 múltiplos del multiplicando disponibles antes de comenzar la multiplicación. Por ejemplo con c=2, entonces x, 2x, 3x deberán estar listos. Si los bits Yi y Yi+1 del multiplicador son: 00 se hace un corrimiento a derecha de longitud dos. 01 se suma x antes del corrimiento. 10 se suma 2x ???corre????? 11 se suma 3x en el paso 2 del algoritmo original, i se incrementa en 2.
18
Este esquema de recodificación pude ser combinado con los saltos sobre 0’s y 1’s. La tabla posterior indica como se podría hacer esto para dos bits por vez con la generación de solamente 2x y 4x, es decir, solamente con corrimientos a la izquierda del multiplicando. Para entender la tabla se debe prestar atención al hecho de que los bits Yi y Yi+1 son los que se recodifican, mientras que el bit Yi+2 se usa para indicar la posible terminación de la cadena. Más aún dos bits extra están implicitos en el extremo menos significativo y un bit extra 0 se asume en el extremo izquierdo. Por ejemplo, el multiplicador de 6 bit(011001) = 25 es recodificado como:
0
01 10 01 00 -4.2^-2.x 2.2^0.x -2.2^2.x 2.2^4.x esto es (32-8+2-1).x = 25x
Yi+2 0 0 0 0 1 1 1 1
Multiplicador
Producto sumado
Yi+1 0 0 1 1 0 0 1 1
0 +2 +2 +4 -4 -2 -2 0
Yi 0 1 0 1 0 1 0 1
Explicación
sin cadena fin de cadena de unos un único uno fin de cadena comienzo de cadena +2 para el fin y –4 para el comienzo comienzo de cadena cadena de unos
Empleo de CSA en multiplicadores: si n operandos tienen que ser sumados, como en el caso de una multiplicación, podemos tomar ventaja de la velocidad(porque no hay suma sino pseudo-suma) de los CSA y la simplicidad(todos full adders) usando el siguiente algoritmo: 1. Setear SSj y CSj a 0, 1 j 2n 2. For i:= 1 step 1 until n do If Yi 0 then For k:= 0 step 1 until (n-1) do 3. Begin SS’i+k SSi+k CSi+k Xi; CS’i+k+1 SSi+k . CSi+k + (SSi+k CSi+k) . Xi; SSi+k SS’i+k; CSi+k+1 CS’i+k+1; End; 3. Sumar SS y CS en un sumador paralelo de 2n bit de acuerdo al sistema (1). Debido a que los CSA son menos complejos que los sumadores paralelos, podemos usar más que uno sin incrementar el costo del multiplicador. Un reloj maneja las operaciones de cada CSA tal que la operación entera, menos la suma ripple, tomaría n niveles de CSA.
19
n-bit path
2-bit path
right shift X.Y3p+1
p iteraciones 0 p n/3
SS
CSA-1 CS
X.Y3p+2
SS
CSA-2 CS
X.Y3p+3
CSA-3 (n-bit) SS
CS
PARALLEL ADDER (2n-bit)
Multiplicador con CSA
Así parecería que no hemos ganado nada de 1 a 3 CSA. Sin embargo si la superposición se permite, por ejemplo la recodificación del multiplicador es hecha en paralelo con la acumulación del producto parcial, hay una mejora definitiva en la duplicación del HW. Más aún, si incrementamos el número de CSA entonces parte del proceso de multiplicación puede ser realizada concurrentemente. A continuación se muestra como la inclusión de 1 o más CSA permite algunas adiciones paralelas. X.Y4p+1
X.Y4p+2
X.Y4p+3 X.Y4p+4
CSA-1 SS
CSA-2 SS CS
CS CSA-3 SS
CS CSA-4 SS
CS
Multiplicador con 4 CSA’s El número de iteraciones fue ahora decrementado de n/3 a n/4 para un tiempo total de 3n/4 niveles de CSA. Más generalmente, si tenemos (n-2) CSA’s podemos construir un árbol de profundidad del O(log n), es decir, generar la suma hasta la adición ripple en un O(log n) niveles de CSA. Este orden de magnitud se obtiene por la siguiente construcción: Agrupamos los n operandos de a 3 como entradas a un nivel tope de CSA. Obtenemos aproximadamente 2n/3 salidas que pueden alimentar de a tres a un segundo nivel de CSA. Continuamos con esta construcción hasta que queden solamente dos salidas. Evidentemente tenemos una profundidad logarítmica ya que decremantamos el número de entradas en 1/3 en cada nivel. Se prueba por inducción sobre el número de operandos 20
que con (n-2) CSA se puede construir un árbol cuya complejidad es logarítmica en n y que resuelve el producto en una sola pasada. Demostración: supongamos tener un multiplicador de n operandos Caso base:
n=3, entonces n-2=1 CSA XYp+1
XYp+2 XYp+3
SS
CS
Multiplicador de n operandos Paso : supongamos que vale para n, dpq vale para n+1, es decir, qpq con (n+1)-2 CSA se puede resolver la multiplicación con una complejidad logarítmica de n+1 en una sola pasada. Por hipotesis inductiva tenemos que n-2 CSA resuelven el producto de n operandos en una sola pasada, obteniendo en un último nivel un SS y un CS. Para n+1 vale ya que podemos conectar las dos salidas anteriores al nuevo CSA con el nuevo operando, utilizando así n-1 CSA.
CSAn-2 SS
operando n+1 CS
CSAn-1 SS
CS Agregando un CSA
Tal configuración se denomina Wallace Tree. En general no se desea invertir (n-2) CSA, sino un número limitado de ellos implementando para esto un proceso iterativo. A continuación se muestra un Wallace Tree con 14 CSA para sumar 16 operandos(de una sola pasada):
21
X1 X2 X3
CSA-1
X4 X5 X6
X7 X8 X9
X10 X11 X12
X13 X14 X15
CSA-2
CSA-3
CSA-4
CSA-5
CSA-6
CSA-7
CSA-9
X16
CSA-8
CSA-10
CSA-11
CSA-12
CSA-13
CSA-14
El proceso Log-sum: si en lugar de tener un hardware multiplicador para 2 operandos de n-bit, tenemos dos multiplicadores, cada uno de los cuales puede manejar un multiplicando de n-bit y un multiplicador de n/2 bits, o sea podríamos hacer en paralelo las multiplicaciones del multiplicando por la mitad superior del multiplicador y por su mitad inferior. Si el tiempo de multiplicación completa es: T1 = n(A+S), Donde A es el tiempo para la suma y S es el tiempo de corrimiento, entonces con dos multiplicadores se tiene: T2 = n/2 . (A+S) + A Tiempo para la suma de los dos productos resultantes. Tiempo de multiplicar en paralelo. Podemos generalizar el proceso para m multiplicadores y lograr una velocidad de: Tm = n/m .(A+S) + log m .A En el límite tenemos 0 (log n) sumas de operandos de n bit. Este proceso log-sum nunca es usado en su totalidad. La duplicación de multiplicadores no es muy usual y limitada a m muy pequeños.
División. División de enteros positivos: la división tiene como entradas un dividendo de (n+m) digitos, x , y un divisor de n digitos, y. Tiene dos resultados, un cociente q de (m+1) digitos y un resto r de n digitos tal que: x = y.q +r,
0r<y
En el método común, asumiendo un sistema númerico posicional de base b, la división implica un mecanismo de prueba y error. Se toma qm para probar:
22
Si x – y.qm < 0 0 x – y.qm < 0
elección errónea, se trata con otro valor. elección correcta .
Una vez que qm ha sido generado , el proceso continúa para qm-1 reemplazando x por x – y.qm hasta llegar a q0. El último dividendo parcial es el resto r. El proceso anterior es llamado división con restoring puesto que restauramos el dividendo cuando hacemos una elección equivocada.. Cuando b=2, hay solo dos elecciones posibles:0 y 1. El algoritmo para x=(xn-1,.......,x0)
y=(yn-1,.......,y0)
q=(qn,.......,q0)
r=(rn-1,.......,r0)
será:
1. Expandir x en x’= (x2n-2,.....,xn,xn-1,.......x0) dejando todas las xi=0(extensión de signo) para n i 2n-2 2n-1????? 2. For i:= 1 step 1 until n do Setear z x’- 2n-i . y If z 0 then qn+1-i 1 y x’ z ojo!!!!!!! Else qn+1-i 0 y no modificamos x’ 3.
r x’
este algoritmo puede ser implementado con un substractor y un registro de desplazamiento de 2n-bit. La parte del else del paso 2 dice que si la substracción dá un resultado negativo, tenemos que hacer una suma para restaurar x’. esto puede evitarse usando la técnica sin restoring. Con respecto al paso 2 del algoritmo anterior, cuando z es negativo(hemos optado mal) tenemos: x’- 2n-i .y < 0 luego de esta substracción y asumiendo que i n, sumamos 2n-i .y y restamos 2n-i-1 .y ,i.e., hacemos: z1 x’ – 2n-i .y + 2n-i .y - 2n-i-1 .y ,ó z1 x’- 2n-i-1 .y si en lugar de esto no retauramos z y sumamos 2n-i-1 .y cuando z es negativo, obtenemos: z2 x’ – 2n-i .y + 2n-i-1 .y ,ó z2 x’- 2n-i-1 .y = z1 la única dificultad con este método es cuando el último bit del cociente lleva un resto negativo. Esto sucede si tenemos para algún j: r’ x’ – 2j+1 .y + (2j, 2j-1,.......,20).y < 0 el resto correcto debería haber sido el resto parcial generado al mismo tiempo que el j-esimo bit del cociente, esto es: r r’+ y Entonces, el algoritmo sin restoring para enteros positivos binarios resulta: 1. Expandir x en x’ como en el algoritmo con restoring; signo 1 2. For i:= 1 step 1 until n do Setear z x’- signo . 2n-i . y If z 0 then qn+1-i 1 y signo 1 ojo!!!!!!! else qn+1-i 0 y signo -1; x’ z;
23
3.
If q0 = 1 then r x’ else r x’+y;
División sin restoring en complemento a 2: dados x e y, queremos que q y r sean tales que: con r y x del mismo signo y 0 |r| < |y|
x = y.q+r,
La primera modificación al algoritmo para enteros positivos es una diferente extensión de signo. Si x e y son cantidades de (n+1)-bit, con xn y yn siendo sus respectivos signos, el primer bit qn del cociente deseado de (n+1)bits será: qn xn yn Si el dividendo(parcial) z(inicialmente el x extendido) e y son del mismo signo, entonces restamos y(sumamos su complemento a 2). Por el contrario, si z e y son de signos distintos, entonces sumamos y. En ambos casos tenderá a decrementar el valor absoluto de z. Cuando esto se aplica al primer dígito del cociente obtenemos: Z x – (1-2qn).2n-1 .y, Donde el x sobre el lado derecho de la expresión es un x extendido. Si el dividendo parcial resultante z e y tienen el mismo signo, entonces qn-1 debe ser seteado a 1. Por ejemplo, asumiendo x, y, z positivos, qn debería ser 0 y podemos razonar como en el caso de la división de enteros positivos. Si las tres cantidades son negativas, qn es 0 y tratamos de decrementar x en valor absoluto. Si nuestro resultado es otra vez negativo, la elección fue correcta y el primer bit del cociente positivo debería ser 1. Si ahora x es positivo, y es negativo, es decir qn es 1, y el z resultante es negativo, como y, nuestra elección fue errónea debido a que 2n-1 .y es mayor en valor absoluto que x. deberíamos poner un 0 en el bit del cociente(elección errónea), pero ya que este último está en la representación complemento a 2 ponemos el complemento, esto es 1. El mismo tipo de razonamiento se puede aplicar a los 8 casos restantes. Podemos repetir el paso previo de arriba hasta el último bit del cociente qo. En este punto tenemos que resolver dos problemas. El primero es que deseamos tener un resto r y un dividendo original x del mismo signo; y el segundo está relacionado con la representación 2 complemento, la cual diferirá en 1 si el cociente es negativo, ya que en escencia hemos generado el complemento a 1. Así si el último dividendo parcial generado, sea z0 y x son del mismo signo podemos setear r a z0 y q0 a 1. Esto es evidente si y es tambien del mismo signo. Por otro lado si y fuera del signo opuesto, entonces r es correcto, q 0 es correcto y así un cero debería ser puesto en esa posición por nuestra discusión previa. La transformación de 1 complemento a 2 complemento sumaría un 1 en la última posición del cociente, así q0 debería ser seteado a uno. Si x y z0 son de diferentes signos tenemos dos casos: Si x e y son del mismo signo(y por lo tanto z0 es de diferente signo), entonces deberíamos setear q0 a 0 y restaurar r como z0+y. Si x e y son positivos este es el esquema que ya hemos visto para enteros positivos. Si x e y son negativos, el cociente es positivo y 0 es el bit generado para una opción incorrecta. Si x e y son de diferentes signos y z0 es del signo de y, entonces deberíamos otra vez corregir r como z0-y. q0 es seteado a 1(opción incorrecta para un cociente negativo) y q es incrementado en 1 para pasar de la representación 1 complemento a 2 complemento.
El algoritmo es entonces como sigue. Sean x=(xn,.......,x0)
y=(yn,.......,y0)
q=(qn,.......,q0)
r=(rn,.......,r0).
X extendido que al mismo tiempo contiene el dividendo parcial será un vector z de 2n-bit 1. Hacer la extensión de signo de x en z = (z2n-1,.......,zn+1,zn,......z0); 2. Qn xn yn; signo qn;
24
3. For i:= 1 step 1 until n-1 do Begin z z-(1-2.signo).2n-i .y; (donde la resta es la suma en complemento a 2) 4. if z=0 then qn-i 1; qj 0 (0 j < n); r 0 y terminar; else if z2n-i = yn then qn-i 1 y signo 0 else qn-i 0 y signo 1 end; 6. z z-(1-2.signo).y; 7. setear r, q0 y q de acuerdo a la tabla posterior(los n-1 bit de más alto orden corresponden al resto)
0 1 2 3 4 5 6 7
xn
yn
zn
0 0 0 0 1 1 1 1
0 0 1 1 0 0 1 1
0 1 0 1 0 1 0 1
Acción q0 1; r z q0 0; r z+y q0 1; r z q0 1; q q+1; r z-y q0 1; q q+1; r z-y q0 1; r z q0 1; r z+y q0 1; r z
El Procesador Central Mirando el ciclo de ejecución de la instrucción notaremos los lugares donde se puede llevar a cabo un mejoramiento de la perfomance. Esto se puede hacer mediante solapamiento de instrucciones, características de look-ahead y unidades multifuncionales. Requerimientos básicos para un PC. Desde el punto de vista de un programador, un PC consiste de: Un conjunto de registros que definen el estado de un sistema.; Un conjunto de registros los cuales contienen operandos y resultados temporarios de computaciones; Una unidad aritmética-lógica(ALU) la cual ejecuta instrucciones de acuerdo a sus opcodes y operandos asociados. Desde el punto de vista del diseñador, hay tres bloques principales: Una unidad de instrucción(I-unit) cuya función es buscar y decodificar instrucciones, para generar las direcciones efectivas de los operandos y para buscar y/o almacenar esos operandos. Despues de esto se pasan las ordenes a una unidad de ejecución la cual hará las operaciones. Una unidad de ejecución(D-unit) la cual puede ser integrada en un único bloque de HW(o un chip), o bien puede estar compuesta de algunas unidades funcionales. Su rol es ejecutar las ordenes enviadas por la I-unit. Registros de almacenamiento, algunos accesibles para el programador, y algunos internos al PC(están tanto en la I-unit como en la D-unit). Por supuesto estos tres bloques están conectados vía control path y data path en una forma transparente al programador quien solo ve los resultados finales de los computos. Pero el diseñador es responsable de la locación de los registros, de la cantidad de concurrencia permitida en la D-unit y las transferencias entre registros, es decir, decisiones las cuales influyen en la velocidad de la computación pero no en la correctitud.
25
Los bloques básicos en una I-unit son: Un Instruction Register(IR) que contiene la instrucción actual. Su parte de opcode puede ser llevada a un decoder(DC), una parte de la unidad de control, que puede activar las líneas de control en la ALU(o D-unit) o activar la transferencia a la primera instrucción de un microprograma para ese opcode, dependiendo de la estructura de control. En el primer caso hablamos de lógica cableada y en el segundo de lógica microprogramada. Un effective address register(EAR) que guarda la dirección de los operandos a ser buscados o almacenados. Este EAR está conectado a la salida de un sumador, el cual puede tomar como entrada el campo dirección del instruction register, y el program counter o un index register seleccionado por el campo index register en el IR. El sumador en si mismo puede ser parte de la D-unit o ser un sumador de “direcciones”(IA) dedicado en la Iunit como se muestra en la figura. Un Program Status Register(PSR) el cual contiene el program counter(PC) y a menudo algunos Flip-Flop(bits) indicando los resultados de condiciones tales como el signo de un resultado, un posible overflow, etc. En el caso de tener que implementar saltos condicionales, el EAR y el PC deberían ser accesibles para la ALU. A continuación se muestra una I-unit de palabras de 16 bits: Desde bus de memoria
16
Registros de almacenamiento
2 16
IR
opcode
campo dirección
program counter
FF’s
PSR
8 6
16 16
DC
2
IA
ALU
16 EAR
ALU 16
16 Hacia (direcciones) bus de memoria : transferencia paralela de a-bits
Los bloques básicos de una D-unit. Su núcleo es una ALU(DA) la cual puede hacer operaciones como suma y resta(en paralelo) de enteros, corrimientos, operaciones lógicas y comparaciones. Sus operandos están contenidos en un acunulador de doble longitud(AQ y MQ) con capacidad de corrimientos y otros registros de longitud simple(R). Algunos Flip-Flop los cuales podrían ser parte de los PSR de la I-unit están seteados por condiciones lógicas tales como overflow y detección de signo. Un contador(C) mantiene cuentas de corrimiento y es usado en los algoritmos de multiplicación y división(estas dos operaciones pueden implementarse mediante lógica cableada o puede usarse microprogramación). Se requierre un acceso desde y hacia memoria principal(MP) para el IR(recibir datos), EAR(enviar direcciones), los registros de almacenamiento SR(enviar y recibir datos), y PC(enviar direcciones). Entonces necesitaremos un bus PC-MP, a menudo llamado memory switch. Tambien se necesita comunicación entre SR, PC, y EAR(IR) y la ALU(o DA) para cargar y almacenar operandos activando el sumador(SR), para valores inmediatos y/o cuentas de corrimientos(PC y EAR), y para retornar direcciones de salto(PC). El D-bus permite esta comunicación. Cuando la 26
concurrencia en el PC es el mayor objetivo en el diseño su diseño y control se hace muy importante y puede ser subdividido en varios data y control paths.
Registros de Almacenamiento
EAR
PC
Bus
C
AQ
MQ
R
Control DA
Control
Control PSR
OV
PSR
SB Control
Latch
Metodos para Acelerar el Ciclo de Instrucción. En camino a lograr acelerar la ejecución de las instrucciones podemos pensar en hacer un solapamiento de las distintas fases que intervienen en la ejecución de las mismas. Para ello es importante determinar que recursos están involucrados en cada fase, los cuales deberán estar libres al comienzo de las mismas y tambien determinar cuales son los recursos que se liberan al final. Esto mostrará que recursos son críticos y eventualmente la posibilidad de duplicarlos para conseguir una mayor concurrencia. Si hacemos un análisis de los recursos utilizados en las distintas etapas de la ejecución de una instrucción tenemos: Paso 1 2 3 4 5
Función
Recursos necesarios
Recursos liberados
Instruction Fetch decodificar e incrementar el PC cálculo de la dirección efectiva busqueda de operandos ejecución
IR, MP Switch, PC IR, DC, PC IA, EAR, IR EAR, MP Switch D-bus, D-unit
PC, MP Switch DC IA, IR EAR, PM Switch D-bus, D-unit
Paso 1: el instruction fetch almacena la próxima instrucción en IR. La dirección de la instrucción es encontrada en PC el cual puede ser modificado cuando termina este paso, y el memory switch(MP Switch) es usado para la transferencia. Paso 2: decodificar e incrementar el PC requiere libre uso del decoder DC y del PC. El primero puede ser liberado luego de hacer su tarea, mientras que el PC no puede tocarse hasta que el paso 1 se invoque nuevamente. Paso 3: el cálculo de la dirección efectiva requiere el uso de IR, algunos de los registros de SR, el sumador IA de la I-unit y los registros EAR. Al final de esta etapa, IR y IA pueden ser liberados puesto que toda la información codificada en la instrucción es extraída(IR), y tenemos a lo sumo una adición en IA por instrucción. Paso 4: búsqueda de operandos requiere el uso de MP Switch y del EAR cuyos contenidos seteados en pasos anteriores no han sido modificados. Al final de este paso, ambos recursos pueden liberarse. 27
Paso 5: toma lugar unicamente en la D-unit y requiere tambien que el D-bus esté libre. Si asumimos que los 5 pasos llevan la misma cantidad de tiempo o en otras palabras que el solapamiento ideal puede ser realizado, todos los recursos los cuales son utilizados por una duración de más de una etapa y cuya información no debe ser modificada deberán ser duplicados para que dicha información no se pierda. En particular esto es cierto para el PC, ya que se hará el fetch de las instrucciones en cada paso. PC tiene que ser incrementado en el paso 1 y algún buffer será necesario para almacenar instrucciones entrantes mientras que el IR todavía tiene la instrucción que está en la etapa de decodificación y para la cual se hace un cálculo de la dirección efectiva(pasos 2 y 3). Si el IR está cargado al final del paso 1 y liberado solo en el final del paso 3, y si una nueva instrucción es encontrada en cada paso, no es dificil preveer el desastre! Tendremos una situación casi continua de overflow en el buffer puesto que se llena dos veces más rápido de lo que se vacía. Podemos dividir los pasos en los tres grupos de acuerdo a los requerimientos: instruction fetch(paso1 y parte del 2), tareas de la I-unit(lo que resta del paso 2, 3 y 4), y tareas de la D-unit(paso 5). Si el MP Switch está restringido a ser accedido secuencialmente y por lo tanto no puede ser tiempo-compartido por el instruction fetch(I-fetch) y la Iunit, entonces o bien la I-unit y la D-unit pueden sobrelaparse o bien el I-fetch y la D-unit pueden trabajar en paralelo. Esto se ve en el siguiente diagrama de Chart: D-unit
i1
I-unit
i1
I-fetch
i3
i2
1
i3
i2
i1
0
i2
2
i3
3
i4
4
5
6
7
Overlap simple Ahora si la MP está organizada de tal manera que es tan fácil buscar una palabra(instrucción) doble como una simple, entonces con agregar un buffer para mantener las palabras extra podemos realizar el sobrelapamiento de la siguiente figura:
D-unit
i1
I-unit
i2
i3
i1 i2
I-fetch
i1 i2
0
1
i3 i4 i3 i4
2
i4
3
4
i5 i6
5
6
7
Tiempo
Overlap con captura de dos instrucciones
Aquí tenemos el primer indicio de look-ahead, esto es , algún trabajo es hecho antes de que se necesite. Este prefecthing es inutil si la instrucción es un salto. Supongamos que t es el(peor caso) tiempo para ejecutar tareas de la I-fetch, I-unit o D-unit, y asumiendo que estos peores casos son los mismos en cada etapa del ciclo de instrucción, entonces tenemos: t0 = tiempo para ejecutar n instrucciones sin overlap = 3nt t1 = tiempo para ejecutar n instrucciones con overlap simple = (1 +2n)t t2 = tiempo para ejecutar n instrucciones con prefetching = (1 + 3n/2)t
28
Esto es, de t0 a t1 hay un mejoramiento del 50% y de t0 a t2 del 100%. El tiempo en el caso de overlap con captura doble de instrucción requiere que la MP sea dividida en dos bancos que puedan ser accedidos independientemente. Esto se conoce como memory interleaving. Con esto logramos reducir los tiempos de acceso. Potenciandolo lo suficiente se puede llegar a hacer busqueda de instrucciones y busqueda de operandos en forma concurrente. La situación ideal se muestra a continuación y además: t3 = tiempo para ejecutar n instrucciones con overlap ideal = (2 + n)t. D-unit I-unit I-fetch
i1 i1, i2 0
i1
i2
i3
i4
i5
i2
i3
i4
i5
i6
i3, i4 1
2
i5, i6 3
4
i7, i8 5
6
7
Overlap ideal con captura doble de instrucción
Pero un doble instruction fetch no es necesario bajo estas condiciones ideales. Entonces la situación óptima sería D-unit I-unit
i1
I-fetch
i1 0
i2 1
i1
i2
i3
i4
i5
i2
i3
i4
i5
i6
i4
i5
i6
i3 2
3
4
i7
5
6
7
Overlap ideal
La ventaja del prefetching es que minimiza la posibilidad de contención de módulos de memoria puesto que las búsquedas de instrucciones y operandos se sobreponen la mitad de las veces. Que otras acciones pueden hacerse para acelerar el ciclo de las instrucciones? Tres áreas donde se puede hacer esta mejora son: Acelerar el instruction fetch; Acelerar el procesamiento en la I-unit; Acelerar el procesamiento en la D-unit. Estos aceleramientos deben ser hechos concurrentemente, ya que sería inútil tener dos etapas extremadamente rápidas y una tercera lenta que actuaría como cuello de botella. Hasta hace poco tiempo el punto débil fue la MP. Con el advenimiento de memorias interleaving, de caches y de memorias semiconductoras sin lectura destructiva, las busquedas de instrucciones y operandos se pueden hacer mucho más rápido. Este hecho influyó en el diseño de los PC en que puede hacerse prefetching más eficiente y algunos operandos pueden estar presentes mientras la Dunit está ocupada procesando instrucciones previas. Por lo tanto serán necesarios buffers para instrucciones, para operandos y para direcciones de operandos
29
Aceleramiento de la Instrucción Fetch. Para acelerar la búsqueda de instrucciones lo ideal sería evitar los accesos a MP. Esto es posible tanto cuando tenemos un Look-ahead como un Look-behind. Se tendrá un buffer de instrucción, implementado con una cola FIFO. El Look-ahead consiste en tener en el procesador un buffer que almacena temporariamente la información que se trae por adelantado, de manera tal que cuando esta se necesita, ya se dispone de ella. El Look-behind consiste en que una instrucción permanece en el buffer de instrucción decodificada, o más aún ejecutada, hasta que las m instrucciones que le sucedan hayan sido decodificadas. Con esto, si se dá la situación típica de saltos hacia atrás dentro de la ejecución de un programa, en la medida que el salto esté contenido en el buffer, solo tendría que modificar un puntero al buffer y evito el acceso a memoria por medio del MP Switch.
Look-Ahead Instruction Register Look-Behind
Cuando se completa la parte destinada a Look-Behind se pierde la instrucción que ha estado más tiempo almacenada reemplazandola por la nueva instrucción. Los límites dentro del buffer para el Look-Ahead y Look-Behind varian. Esta forma de trabajo es similar al funcionamiento de memoria caché. Aceleramiento de la I-unit. El mayor tiempo que se consume en esta etapa es la búsqueda del operando. Como antes la mejor forma de minimizar esta operación es evitandola. Esto significa que las instrucciones registro a registro deberán ser usadas tanto como sea posible(sin embargo los resultados que se obtienen en estas máquinas no son siempre los ideales). Vector processors usan características interleave para máxima eficiencia. Aceleramiento de la jerarquía de memoria con la introducción de buffers rápidos, o caches, benefician los tiempos de ejecución en la I-unit, aunque la aleatoreidad del acceso a datos comparado con la secuencialidad de las instrucciones no favorece a la eficiencia. Aceleramiento de la D-unit. En esta etapa uno primero podría utilizar algoritmos eficientes. En general esto implicará que estén presentes varias D-units, cada una dedicada a un conjunto de funciones. Cuanto más dedicadas sean estas unidades, superior va a ser la perfomance. Esta ganancia puede ser mejorada si permitimos que varias Dunit operen en paralelo. Si permitimos superposición en las ejecuciones de la D-unit, entonces necesitaremos algún mecanismo de control de HW, el cual puede chequear el orden permisible de las operaciones. Estos controles pueden hacerse muy complejos. De la misma manera, si la concurrencia es permitida en la ejecución, entonces los programas deberán ser modificados de manera tal que tomen ventaja del modo de operación. Esto implica detección de paralelisno y scheduling.
Look-Ahead y Paralelismo. Modelos de procesadores Look-Ahead: el objetivo en el diseño de procesadores con Look-Ahead que permiten terminaciones no secuenciales(una instrucción puede terminar luego de que una de sus sucesoras haya terminado) es controlar la ejecución de las instrucciones, de manera tal que los resultados obtenidos son los mismos que aquellos que hubieran resultado con una ejecución secuencial estricta. Más aún nos gustaría obtener un tiempo de terminación mínimo. La complejidad de la implementación crecerá con el deseo de ser tan optimo como sea posible. 30
Condiciones que impiden que una instrucción sea despachada. Las instrucciones son buscadas, en una manera look-ahead, en un buffer de instrucción. Luego, si la parte de decodificación de la I-unit puede permitir que la instrucción proceda o bien puede impedir que la instrucción sea despachada. Además cuando una instrucción no puede ser despachada, por condiciones que ya veremos, instrucciones subsecuentes no podrán ser despachadas tampoco. Estas condiciones son: La primera condición, la cual está orientada a los recursos, es la indisponibilidad de que una unidad funcional haga su tarea. Ejemplo: si asumimos un simple sumador, con un tiempo de suma de 4 unidades y un multiplicador simple con un tiempo de multiplicación de 10 unidades, la secuencia: S1: R1R2+R3 S2: R4R2*R5 S3: R6R3+R6 Esta secuencia produce el siguiente diagrama: I-f
I-u I-f
Sumar I-u I-f
Multiplicación delay
I-u
Sumar
Se necesita una D-unit que está ocupada en este momento, por lo tanto no será despachada a la fase de decodificación. El segundo factor que puede impedir que una instrucción sea despachada es relativa al procedimiento. Esto sucede cuando dos instrucciones son dependientes una de la otra, esto es cuando el orden en el cual ellas son completadas tienen un impacto sobre los valores que están siendo computados. Si Si y Sj son dos instrucciones, sean Ii y Ij sus respectivos dominios y Oi y Oj sus rangos. Si Si y Sj tienen un flujo de control secuencial, entonces estas dos instrucciones pueden ser ejecutadas en paralelo sssi: 1) No almacenan valores en la misma variable(registros). 2) El resultado de una instrucción no es el operando de entrada para la otra. Traduciendo estos requerimientos en terminos de dominios y rangos: Oi Oj = Oi Ij = ,para todo (i,j),ij Oj Ii = Si probamos con el ejemplo anterior vemos que las tres operaciones pueden ser procesadas en paralelo desde el punto de vista procedural. En el diseño de nuestro procesador simple, necesitamos mantener información de la disponibilidad de la D-unit y del uso de los registros. La primera tarea es fácil: asociamos un bit de “ocupado” con cada unidad. Cuando bh, el bit asociado con Dh , es 0, una instrucción puede ser despachada a Dh . En este punto, bh es seteado a 1 y esto impide cualquier despacho hacia Dh. Cuando se completa la operación en Dh, bh es reseteado a 0. Si uso la misma estrategia para los registros, esto es, sea rk el bit asociado con Rk, este podría ser seteado a 1 cuando Rk está en el rango de una instrucción despachada. Esto podría deshabilitar futuros despachos de cualquier instrucción que tenga Rk tanto en su rango como en su dominio. Cuando Rk recibe su valor, al final de la instrucción para la cual es una variable de salida, rk puede ser reseteado a 0. Este esquema falla, sin embargo, cuando consideramos el caso donde Rk está en el dominio de la instrucción Si(Rk Ii) y en el rango de Sj(Rk Oj) la cual sigue a Si. La tercera condición (Oj Ii = ) no es satisfecha y Sj no puede ser procesada ya que existe el riesgo de tener su resultado almacenado en Rk, antes de haber usado a Rk como un operando de Si. Seteando un bit Rk, cuando Rk está tanto en el dominio como en el rango de Si resolveriamos 31
este conflicto, pero no permitiría la ejecución concurrente de dos instrucciones que tienen elementos comunes en sus dominios. Como algunas instrucciones pueden tener dominios no-disjuntos, y ya que todas pueden ser completadas antes que un elemento común pueda ser parte del rango de una instrucción subsecuente, necesitaremos contadores para monitorear la utilizaciónde los registros. Sea Ck el contador asociado con Rk. Inicialmente Ck es seteado a 0. Cuando Rk está en el rango de alguna instrucción Si, su contador es seteado a 1. Si está en su dominio, Ck es decrementado en 1(si está en el rango y en el dominio, es seteado a 1 ya que ninguna otra instrucción puede usarlo). Cuando se completa la instrucción, todos los elementos de su rango tienen sus contadores reseteados a 0 y todos los elementos en su dominio tienen sus contadores incrementados en 1(con la misma excepción de arriba, donde el contador será reseteado a 0) (ver ejemplos!!!!)
Unidades D-virtuales: un número pequeño de unidades funcionales y las dependencias de procedimiento contribuyen al retardo de las operaciones. La primera acción de retardo puede ser solucionado teniendo varias Dunit que puedan ejecutar las mismas operaciones. Si tales inversiones de HW son demasiados costosas, podemos usar unidades D-virtuales como sigue. En lugar de no despachar una instrucción a una D-unit ocupada, el requerimiento es puesto en una cola FIFO asociada a la unidad. Cuando la D-unit se libera, selecciona la primera instrucción(si la hay) en la cola. La ventaja es que ahora solo las dependencias relativas al procedimiento impiden el despacho de instrucciones. Ejemplo: asumamos que integramos I-fetch e I-unit con un tiempo de 1 ciclo, el tiempo de suma de 2 ciclos y un tiempo de multiplicación de 4 ciclos. Las unidades de suma y multiplicación son únicas. Consideremos la secuencia: S1: R1R2+R3 S2: R2R4*R5 S3: R3R3+R4 S4: R6R6*R6 S5: R1R1+R5 S6: R2R3+R4 Si asumimos que tenemos unidades D-virtuales, el diagrama de Gantt es el siguiente: I-u
Suma I-u
Multiplica
I-u
Suma
I-u
Multiplica I-u
Suma I-u
Suma
Look-Ahead con unidades D-virtuales Reproducir D-unit, tanto en el sentido real, o con la ayuda de colas FIFO, es necesariamente limitado. Se debe seleccionar un límite en el número de instrucciones que pueden ser despachadas. Este número puede estar relacionado con la frecuencia de ocurrencia de instrucciones específicas, sus tiempos de ejecución y la capacidad del buffer look-ahead. Cuando la cola de una D-unit está llena, el despacho de una instrucción a la D-unit debería estar prohibida hasta que la instrucción actual se complete. En esencia, reemplazamos el bit de ocupado introducido
32
anteriormente, por un contador inicializado con la longitud máxima de la cola(o el número máximo de unidades). Con cada instrucción despachada a una unidad particular, el contador es decrementado en 1, y es incrementado en 1 cuando la instrucción se completa. El test del bit ocupado es reemplazado por testear si el contador es 0. Las D-unit podrían tener sus propios buffers de operandos. Si así fuera, la condición Oj Ii = no es requerida en razón de permitir concurrencia entre la instrucción Si y su sucesora Sj. Al momento del despacho de Si, los contenidos de los registros en sus dominios son transferidos en los buffers internos en la D-unit que va a realizar la operación. Pagamos un precio por esta transferencia pero se aumenta el paralelismo. Además el control se simplifica ya que no hay necesidad de un contador/registro y podemos retornar al esquema original del simple bit. De la misma manera los resultados pueden ser almacenados en la D-unit esperando la finalización de operaciones previas(buffers a la salida). Entonces la condición Oi Oj = puede tambien ser eliminada. Un segundo bit por registro se necesita para propositos de control, de manera tal que a lo sumo un almacenamiento de retardo ocurra para un dado registro. Conflictos de la forma Ij Oi = permanecerán ya que tienen implicaciones lógicas en la correctitud de los resultados. Para el ejemplo anterior, asumiendo look-ahead con buffers de entrada y sin unidades virtuales, el diagrama de Gantt sería: S1 S2
S3
I-u
Suma I-u
Multiplica
I-u
S4
Suma
I-u
S5
Multiplica
I-u
S6
Suma I-u
Suma
Obs: la primera multiplicación no se retrasa nunca más. Asumiendo unidades virtuales buffers para los registros, podemos aún mejorar nuestro procesador look-ahead? El siguiente ejemplo muestra que mientras algunos conflictos relativos al procedimiento no pueden ser evitados, no implica que se deba parar el despacho de instrucciones subsecuentes Ejemplo: (a) consideremos la secuencia: S1: R1R2+R3 S2: R4R1*R5 S3: R6R3+R6 Como hay un conflicto de procedimiento entre S1 y S2, la última no podría ser despachada si seguimos las reglas respetadas hasta ahora. Sin embargo, si se usa despacho condicional reservando la unidad de multiplicación para ella, e indicando cuales registros se necesitan y cuales van a ser modificados por la instrucción, entonces podriamos despachar y ejecutar S3 .
33
(b) consideremos la secuencia: S1: R3R1/R2 S2: R4R1*R3 S3: R1R2+R5 No es difícil ver que si S2 es condicionalmente despachada como en la secuencia previa, entonces S3 pueda ser procesada y completarse antes del comienzo de S2(asumimos que la división toma 6 unidades de tiempo). Por lo tanto, debemos prevenir el almacenamiento del resultado de la suma en R1 hasta el tiempo de comienzo de la multiplicación. Este ejemplo muestra que si queremos implementar “forwarding” necesitamos un dispositivo de control el cual indique cuando valores asociados con registros están disponibles, así como tambien cuando resultados pueden ser almacenados en los registros. Para esto podemos asociar a cada buffer de entrada de la D-unit un bit indicando si un valor está en el buffer o si está esperando que se complete una operación en alguna otra D-unit. En este caso, en vez de que un valor sea almacenado en el buffer, ponemos en el una marca(tag) que identifique la D-unit que enviará el valor. Cuando los bits de ambos operandos están seteados a “valores” la operación puede proceder. Cuando una instrucción en la D-unit se completa el control busca todas las marcas en las otras D-units, las cuales están esperando por una finalización particular. La transferencia desde un registro salida a los buffers es entonces autorizada y esta podría permitir el comienzo de algunas operaciones despachadas condicionalmente. Es evidente que esta busqueda lleva tiempo, a menos que pueda ser realizada en una manera asociativa o a menos que haya un número limitado de unidades(i.e, la longitud de las colas se restringe)
Diseño de Control del PC. La computadora está dividida en componentes básicos: el CPU, o el procesador, es el centro de la computadora y contiene todo excepto la memoria, entrada, y salida. El procesador, además, está dividido en computación (Datapath) y control. Datapath: consiste de unidades de ejecución, tales como ALUs, los registros, y los caminos de comunicación entre ellos. Desde el punto de vista del programador, el datapath contiene la mayor parte del estado del procesador- la información que deberá ser salvada por un programa que va a ser suspendido y restaurado luego para continuar ejecutandose. Además de los registros de proposito generale visible para el usuario, el estado incluye el PC, el registro de dirección de interrupción(IAR), y el registro de estado del programa(flags). Como una implementación es creada para una tecnología de HW específica, es la implementación la que establece el tiempo de ciclo de reloj. El tiempo de ciclo de reloj está determinado por los circuitos más básicos que operan durante un período de ciclo de reloj- dentro del procesador, el datapath frecuentemente tiene este honor. El dataparh tambien dominará el cosro del procesador, tipicamente requiere la mitad de los transistores y la mitad del área del procesador. Además se puede decir que es la parte más simple del diseño del procesador. Tambien condiciona la mejor perfomance ya que modula el CPI(ciclos por instrucción) y el CLOCK RATE(frecuencia de reloj). La operación fundamental del datapath es leer los operandos del banco de registros, procesarlos en la ALU, y luego guardar los resultados. Control Cableado: si el diseño del datapath es simple, entonces alguna parte del diseño del procesador deberá ser dificultosa, y esta parte es el control. El control puede ser simplificado- la forma más facil es simplificar el set de instrucciones, pero el número de instrucciones depende de la arquitectura de la computadora. La unidad de control dice al datapath que hacer en cada ciclo de reloj durante la ejecución de las instrucciones. Esto es tipicamente especificado por un diagrama de estados finitos. Cada estado corresponde a un ciclo de reloj, y las operaciones que se hacen durante el ciclo de reloj se escriben dentro del estado. Cada instrucción toma algunos ciclos de reloj para completarse. Una porción de un diagrama de estados finitos para los dos primeros pasos de la ejecución de una instrucción podría ser:
34
MARPC
Acceso a memoria no completo IRM[MAR] Acceso a memoria completo PCPC+4 ARs1 BRs2
En el primer estado el registro de dirección de memoria(MAR) es cargado con el PC, el registro de instrucción es cargado desde menoria en el segundo estado y el PC es incrementado en eltercer estado. Este tercer estado tambien carga los dos registros de operandos, Rs1 y Rs2, en los registros A y B para usar en estados posteriores. Ahora hay que llevar este diagrama de estados al HW. Las alternativas para hacer esto dependen de la tecnología de implementación. Supongamos que el diagrama finito tiene 50 estados. Entonces se requieren 6 bits para representar el estado. Entonces, las entradas al control deberán incluir estos 6 bits, algún número de bits(3) para seleccionar condiciones desde el datapath y la unidad de interface de memoria, más los bits de instrucciones(12). Dadas estas entradas el control puede ser especificado por una gran tabla. Cada fila de la tabla contiene los valores de las líneas de control para hacer las operaciones requeridas por ese estado y reemplazar el próximo número de estado. Asumimos que existen 40 líneas de control.
Control L I N E A S
Lógica Cableada 40 bits
D E
212+3+6=221entradas
12
3
C O N T R O L
6 State
Datapath
Instruction Register
Reducir costos de HW en control cableado: la implementación más sencilla de una tabla es con una ROM. En este ejemplo, 221 palabras de 40 bits cada una, requeririan una ROM de 10 MB. Como solo parte de esta tabla tiene información útil, el tamaño puede ser reducido manteniendo solo las filas con información única- con el costo de una decodificación de direcciones más complicada. Este HW se llama PLA. Esencialmente reduce el HW de 221 35
palabras a 50 palabras mientras se incrementa la lógica de decodificación de direcciones. Otra forma de reducir los requerimientos de HW es minimizando el número de miniterms(filas de la tabla). Si el tamaño del PLA crece mucho, una gran tabla puede ser factoreada en algunos PLA más chicos, cuyas salidas son multiplexadas para seleccionar el control correcto. Curiosamente, la numeración de los estados en el digrama de estados finitos puede hacer la diferencia en el tamaño del PLA. La idea es tratar de asignar números de estados similares a estados que hacen operaciones similares. Diferenciando los patrones de bits que representan el número de estados mediante un único bit(problema de asignación de estados). Como los bits de instrucción son tambien entradas al control del PLA, ellos pueden afectar la complejidad del PLA así como lo hace la numeración de estados. Por este motivo se deberá tener cuidado al seleccionar opcodes puesto que pueden afectar el costo del control. {++El control cableado ve a la unidad de control como un circuito lógico, que genera unas dadas secuencias fijas de control. Como tal es diseñado con los objetivos usuales de minimizar el número de componentes usados y maximizar la velocidad. Un sistema tal, una vez construido no podrá cambiarse a menos de rediseñarlo y recablearlo.++} Perfomance del control cableado: cuando diseñamos el control para una máquina, queremos minimizar el CPI promedio, el ciclo de reloj, la cantidad de HW para especificar control y el tiempo en desarrollar el control correcto. Minimizar el CPI significa reducir el número medio de estados que componen la ejecución de una instrucción, porque cada ciclo de reloj corresponde a un estado. Esto tipicamente se logra haciendo cambios al datapath combinando o eliminando estados. Por ejemplo, si se cambia el HW tal que el PC puede ser usado directamente para direccionar memoria sin pasar por el MAR primero. Entonces el diagrama de estdados sería:
Acceso a memoria no completo IRM[PC] Acceso a memoria completo PCPC+4 ARs1 BRs2
Este cambio ahorra un ciclo de reloj(un estado menos) por cada instrucción. Control Microprogramado: la solución de Wilkes fue poner la unidad de control en una computadora en miniatura teniendo una tabla para especificar el control del datapath y una segunda tabla para determinar el flujo de control al nivel “micro”. Wilkes llamó a su invención microprogramación y asoció el prefijo “micro” a los terminos tradicionales usados en el nivel de control: microinstrucción, microcódigo, microprograma y así siguiendo. Las microinstrucciones especifican todas las señales de control para el datapath, más la capacidad de decidir condicionalmente cual será la próxima microinstrucción a ejecutarse. Como el nombre”microprogramación” sugiere, una vez que se diseño el datapath y la memoria para las microinstrucciones, el control se convierte esencialmente en una tarea de programación.; esto es, la tarea de escribir un interprete para el set de instrucciones. La microprogramación permite que el set de instrucciones sea cambiado alterando los contenidos donde está almacenado el control(la mayoria de las máquinas usan ROM) sin tener que tocar elHW.
Una organización para un control microprogramado simple podría ser:
36
Control
Microprograma (ROM)
L I N E A S
40 bits
12+3+6
2
D E
21
=2 entradas
C O N T R O L
Microinstruction PC
1
+
Datapath
Lógica para selección de direcciones Instruction Register
Aquí, a diferencia del control cableado, hay un incrementador y una lógica especial para seleccionar la próxima microinstrucción. Existen dos approaches para seleccionar la próxima microinstrucción: usar un program counter para la microinstrucción, como se muestra arriba, o incluir la dirección de la próxima microinstrucción en cada microinstrucción. Principios de la Microprogramación: las líneas de control que hacen funciones relacionadas, generalmente se ponen una despues de otra para fácil comprensión. Grupos de líneas de control relacionadas son llamadas campos y dan formato a la microinstrucción. Cada campo tiene un nombre que refleja su función. Un ejemplo de una microinstrucción con 8 campos: Destino
Operación de ALU
Origen1
Origen2
Constante
Misc
Cond
Jump addres
Se puede usar un PC para suministrar la próxima microinstrucción, pero algunas computadoras dedican un campo en cada microinstrucción para la dirección de la próxima microinstrucción. Algunos hasta proporcionan multiples campos para manejar saltos condicionales. Durante los saltos condicionales se podría decodificar una instrucción mediante el testeo de su opcode un bit a la vez, pero approuch es demasiado pobre en la práctica. El esquema de decodificación de instrucciones más rápido ¿??????. Un approach más refinado es usar el opcode para indexar una tabla que contiene direcciones de microinstrucción que suministra la próxima dirección. La memoria donde está el microprograma, o almacenamiento de control(control store) es el HW más visible y más facil de medir en control microprogramado; por lo tanto aquí se focalizan las técnicas para reducir los costos de HW. Las técnicas para reducir el tamaño del control-store incluye reducción del número de microinstrucciones, reducir el ancho de cada microinstrucción, o ambos. Así como los costos están medidos por el tamaño del controlstore, la perfomance tradicionalmente se mide por los CPI. El microprogramador conoce la frecuencia de las macroinstrucciones, y por lo tanto conoce donde y como se gasta la mayor parte del tiempo- las instrucciones que demandan la mayor parte del tiempo de ejecución son optimizadas en velocidad, y las otras son optimizadas para espacio. Las técnicas de control, cableada o microcódigo, son juzgadas por su impacto en el costo del HW, tiempo de ciclo de reloj, CPI y tiempo de desarrollo.
37
Tecnicas para reducción de costos. Reducción de costos de HW mediante la codificación de líneas de control: el approuch ideal para reducir el controstore es primero escribir el microprograma en una notación simbólica y entonces medir como se ponen las líneas de control en cada microinstrucción. Con estas medidas podremos ser capaces de reconocer los bits de control que pueden ser codificados en un campo mas pequeño. Si no más que una de 8 lineas estan seteadas simultaneamente en la misma microinstrucción, entonces ellas pueden ser codificadas en un campo de 3 bits(log2 8 =3). Este cambio ahorra 5 bits en cada microinstrucción y no perjudica a los CPI, sin embargo significa un costo extra de HW de un decoder de 3 a 8 necesario para generar las 8 líneas de control originales. O sea que sacar 5 bits del control-store usualmente nos trae el costo de un decoder. Esta técnica de reducir el ancho de campo se llama codificación. Existen peligros en la codificación. Por ejemplo, si una línea de control codificada está sobre el critical timing path, o si el HW que lo controla está sobre el camino crítico, entonces padece el tiempo de ciclo de reloj. Otro peligro puede ser que en la última revisión del microcódigo se podrían encontrar situaciones donde las líneas de control podrían ponerse en la misma microinstrucción, perjudicando la perfomance o requiriendo cambios en el HW que podrían aumentar el ciclo de desarrollo. Reducción de costos de HW con formatos múltiples de instrucciones: las microinstrucciones pueden achicarse si ellas son divididas en diferentes formatos y proporcionan un opcode o campo de formato para distinguirlas entre ellas. El campo de formato proporciona a todas las líneas de control sin especificar sus valores por defecto. Reducir el costo de HW usando campos de formato tiene su propio costo de perfomance que es ejecutar más instrucciones. Generalmente, un microprograma que usa formatos de microinstrucción simple puede especificar cualquier combinación de operaciones en un datapath y tomará menos ciclos de reloj que un microprograma hecho con microinstrucciones restrictas. Maquinas estrechas son más baratas porque los chips de memoria son tambien estrechos y altos: se tienen muchos menos chips para palabras de 16K con memorias 24-bit que para palabras de 4K con memorias de 96 bit. Este approuch, estrecho pero alto, es a menudo llamado microcódigo vertical, mientras que el approuch amplio pero bajo es llamado microcódigo horizontal. No existe una definición formal para microcódigo vertical y horizontal. Los terminos relacionados maximamente codificados y minimamente codificados aclarará esta situación. Como una regla, el control-store minimamente codificado usa más bits, y los aspectos estrechos pero altos de los chips de memoria significa que control-stores maximamente codificado naturalmente tienen más entradas. A menudo los diseñadores de máquinas de codificación mínima no tienen la opción de chips de RAM pequeños, causando máquinas con microinstrucciones amplias para terminar con muchas palabras de control-store. Reducción de costos de HW agregando control cableado para compartir microcódigo: el otro approuch para reducir control-store, es reducir el número de microinstrucciones en lugar de su tamaño. Un approuch posible son las microsubrutinas, como tambien las rutinas que comparten código mediante saltos. Se puede hacer que se comparta más código mediante la asistencia de control cableado. Por ejemplo muchas microarquitecturas permiten que bits del Instruction Register especifiquen el registro correcto. Otra ayuda común es usar porciones del Instruction Register para especificar la operación de la ALU. Cada una de estas ayudas está bajo control microprogramado y son invocadas con un valor especial en el campo apropiado. La desventaja de agregar control cableado es que podría ampliarse el ciclo de desarrollo porque no involucra programación, pero se requiere diseño y prueba de nuevo HW.
Técnicas para mejorar la perfomance. Reducción del CPI con microcódigo para casos especiales: el programador experimentado conoce cuando ahorrar espacio y cuando gastarlo. Una instancia de esto es dedicar microcódigo extra para instrucciones frecuentes, y así reducir CPI. Por ejemplo la VAX 8800 usa su gran control-store para muchas versiones de instrucciones CALLs, optimizadas para ahorrar registros dependiendo del valor de una máscara. Reducción del CPI agregando control cableado: agregar control cableado puede reducir costos como tambien mejorar la perfomance. Un ejemplo puede estar en la interface de memoria, donde la solución es que el 38
microcódigo haga un testeo y branch hasta que la memoria este lista(stalls). Un stall se hace cuando una instrucción deberá esperar uno o más ciclo de reloj esperando que algún recurso esté disponible, en este caso la memoria. Muchos approaches solucionan este problema teniendo en el HW stall una microinstrucción que tratará de acceder al memory-data register antes que la operación de memoria se complete. En el instante que la referencia a memoria está lista, la microinstrucción que necesita los datos se puede completar, evitando el reloj-extra de delay para acceder a memoria de control. Reducción del CPI mediante paralelismo: a menudo el CPI puede ser reducido con más operaciones por microinstrucción. Esta técnica, la cual usualmente requiere microinstrucciones amplias, incrementa el paralelismo con más operaciones de datapath. Esta es otra característica de las máquinas llamadas horizontales.
Errores y Riesgos. Riesgo: implementar una función compleja mediante microcódigo podría no ser más rapido que con macrocódigo. Por un lado, microcódigo tiene la ventaja de ser encontrado mucho más rapido en memoria que el macrocódigo. El microcódigo tiene la ventaja de usar registros temporarios internos en la computación, lo cual puede ser útil en máquinas con pocos registros de propósito general. La desventaja del microcódigo es que los algoritmos deberán ser seleccionados antes que la máquina sea anunciada y no podrán ser cambiados hasta el próximo modelo de arquitectura; macrocódigo por el otro lado, puede optimizar sus algoritmos en cualquier momento durante el tiempo de vida de la máquina. Error: si existe espacio en el control-store, no hay costo en poner nuevas instrucciones. Como la longitud del control-store es usualmente una potencia de 2, podría existir espacio disponible para expandir el set de instrucciones. Pero agregar instrucciones generalmente trae costos de labor y mantenimiento. La tentación de agregar instrucciones “libres”(de costo) puede ocurrir solamente cuando el set de instrucciones no está fijo, como sería el caso del primer modelo de una computadora. A causa del requerimiento de compatibilidad en el set de instrucciones, todos los modelos futuros de esta máquina estarán forzados a incluir estas instrucciones “libres”, aún si luego el espacio está muy solicitado. Esta expansión tambien ignora el costo del tiempo de desarrollo y testeo de las instrucciones que se agregaron, como tambien los costos en reparar los bugs. Error: los usuarios encuentran el control-store writable muy útil. Los errores en el microcódigo persuadieron a los diseñadores de minicomputadoras que sería más inteligente usar RAM que ROM para control-store. De esta forma se podrían reparar los bugs del microcódigo con floppy disks perzonalizados en lugar de tener un ingeniero tirando placas y reemplazando chips. Además los usuarios estarían habilitados para escribir microcódigo mediante control-store writable(WCS). Pero a medida que paso el tiempo las WCS no tuvieron el éxito que originalmente se pensó: Las herrramientas para escribir microcódigo fueron mucho más pobres que aquellas para escribir macrocódigo A la vez que la memoria principal se fue expandiendo, WCS limitó a instrucciones de 1-4KB. Se utilizó el set de macroinstrucciones para construir el microcódigo, de esta manera el microcódigo se hizo menos útil para algunas tareas para las cuales fue pensado. Con el advenimiento del tiempo compartido, los programas podían correr por solo milisegundos antes de cambiar a otras tareas. WCS tendría que ser swapped si más de un programa lo necesitaba, y recargar el WCS podría tomar algunos milisegundos. El tiempo compartido tambien significó que los programas deberían protegerse unos de otros. A causa de su bajo nivel, los microprogramas pueden burlar toda barrera de protección. Microprogramas escritos por usuarios eran muy poco confiables.
39
Aplicaciones de la Microprogramaciรณn. Ademรกs de interpretar un dado conjunto de instrucciones, la microprogramaciรณn puede usarse en otras tareas de naturaleza interpretativa, como ser emulaciรณn, ejecuciรณn directa de lenguajes de alto nivel, potenciamiento de aspectos especiales de una dada arquitectura. Emulaciรณn: la microprogramaciรณn es la interpretaciรณn de un conjunto de instrucciones nativo. La emulaciรณn serรญa la interpretaciรณn de un set de instrucciones diferente al set nativo. Un emulador microprogramado serรก entonces un microprograma en una mรกquina ejecutando los 6 pasos del ciclo de ejecuciรณn de instrucciones de otra mรกquina. Para ser eficiente, el PC emulado, la palabra de estado y toda variable de estado del procesador emulado deberรก estar en registros accedidos por el microcรณdigo de la mรกquina original. Naturalmente, la velocidad de emulaciรณn dependerรก de la arquitectura de la mรกquina original y de la emulada. Si el set de instrucciones se parece y los tipos de datos bรกsicos tiene igual longitud, la emulaciรณn serรก rรกpida. Se puede concebir que una versiรณn de una mรกquina emulada A en una mรกquina B puede ser mรกs rรกpida que la versiรณn original, si B es mรกs rรกpida que A(ciclos mรกs rรกpidos Mp, CM mรกs rรกpida y con horizontalidad mayor que A). Lo visto para emulaciรณn es relativamente simple. Requiere el entendimiento de la instrucciรณn a ser emulada y de las habilidades de la microprogramaciรณn de la mรกquina en la cual se implementarรก. Cuando se incluyen instrucciones con privilegios y E/S en la emulaciรณn, se agrega otro nivel de complejidad(se deberรกn manejar buffering, interrupciones y excepciones). Ejecuciรณn directa de lenguajes de alto nivel: la traducciรณn y posterior ejecuciรณn de un programa escrito en alto nivel puede adoptar dos tรฉcnicas. En un proceso de compilado todo el programa fuente es traducido a lenguaje mรกquina, luego el lenguaje mรกquina a objeto es ejecutado. En un proceso de intรฉrprete, el fuente es ejecutado sin pasar por la fase de programa objeto. Esto pyede llevar a procesos ineficiente, dado que una sentencia debe ser analizada repetidamente cada vez que se encuentra. Luego el intรฉrprete es dividido en dos partes: una de traducciรณn que transforma el lenguaje fuente en un lenguaje intermedio y una fase de ejecuciรณn que implementa directamente el cรณmputo de string intermedio. Veamos que pasa cuando la mรกquina es microprogramada en el caso de interprete. La rutina que interpreta puede ser escrita en lenguaje mรกquina(a su vez ejecutada por el microcรณdigo) o puede ser microprogramada. En este รบltimo caso se tiene lo que se conoce como ejecuciรณn directa de lenguajes de alto nivel. El lenguaje intermedio, o lenguaje de ejecuciรณn directa, tiene que ser una buena salida para la fase de traducciรณn y una buene entrada para la interpretaciรณn del microprograma, esto implica una serie de condicionamientos.
Datos Lenguaje Fuente
Lenguaje Objeto
Datos de Salida
Compilador
Ejecuciรณn
Datos Lenguaje Fuente
Lenguaje Intermedio Fase de Traducciรณn
Datos de Salidas Rutinas Interpretativas
40
Datos
Lenguajes Intermedios
Rutinas de Lenguaje Máquina
Rutinas Microprogramadas Rutinas Interpretativas
Datos
Lenguajes Intermedios
Ejecución directa vía Rutinas Microprogramadas Rutinas Interpretes
a) Proceso de compilado b) Proceso de interpretación c) Y d) Procesos de interpretación (alternativos).
Sintonía de Arquitectura: la microprogramación dá la opurtunidad al diseñador de modificar y expandir el conjunto de instrucciones clásicas. Por ejemplo instrucciones que computen polinomios, etc. En esencia habría una ganancia en la velocidad de ejecución y un aumento de espacio en la CM. La sintonía del set de instrucciones consiste en monitorear el uso de las mismas, modificando el set nativo, evaluando la ganancia o pérdida en eficiencia, e iterativamente repitiendo este proceso hasta que alcance una mejora razonable. Idealmente se podría hacer el proceso dinámico con máquinas de writable CM. El monitoreo podría hacerse en unas pocas microinstrucciones. El análisis del trace de las instrucciones podría revelar que secuencia de instrucciones pueden ser agrupadas. Un generador de microprograma podrá procesar el microcódigo para tales secuencias e insertar las nuevas microrutinas en el microprograma. La última acción sería reemplazar las secuencias originales por las nuevas instrucciones, interpretadas por las nuevas microrutinas. Sin llegar a ese punto, se puede pensar en optimizar subprocesos por vez(por ejemplo: para sintonizar una línea orientada a editor de texto para una máquina dada, podríamos comenzar por monitorear los comandos más usados y las rutinas que son llamadas más frecuentemente). Se debe tomar la precaución de que en estas instrucciones de ejecución, alternativamente extensas, se puedan atender interrupciones en otros puntos del ciclo de las mismas además del fetch.
Clasificación de los Procesadores Paralelos A los efectos prácticos, el término procesamiento paralelo queda reservado a dos situaciones posibles: Multiprocesador, es decir, tener un sistema con varios procesadores. Un único procesador, pero con capacidad de ejecutar varias instrucciones o efectuar varios cómputos simultaneamente. En camino a hacer una clasificación sabemos que el procesador tiene que hacer: 1) Búsqueda de instrucciones. 2) Búsqueda de operandos. 3) Ejecución. 4) Transferencia de resultados. 41
Y sabemos que existen dos flujos entre el procesador central y memoria: 1) flujo de instrucciones. 2) Flujo de datos. Basandose en esto, Flynn realizo una clasificación de acuerdo a la multiplicidad o nó de los flujos 1 y 2:
SISD: simple instrucción, simple dato. SIMD: simple instrucción, múltiple dato. MISD: múltiple instrucción, simple dato. MIMD: múltiple instrucción, múltiple dato.
Simple instrucción, simple dato(SISD): los sistemas de esta categoría son las computadoras usuales de un único procesador. Esta arquitectura involucra un único procesador, con una única ALU, con capacidad de aritmética escalar unicamente. Múltiple instrucción, simple dato(MISD): no habría conceptualmente máquinas que entraran en esta categoría o clasificación(que muchas instrucciones trabajen con un dato). El pipeline o procesador vectorial si lo interpretamos desde una cierta óptica podríamos incluirlo dentro de esta clasificación. Simple instrucción, múltiple dato(SIMD): una unidad de control(CU) única, busca y decodifica instrucciones. Luego la instrucción es efectuada tanto en la CU o es enviada a algunos Elementos de Procesamiento(PE). Estos PE operan sincronicamente pero sus memorias locales tienen diferentes contenidos. Dependiendo de la CU, el poder de procesamiento, y el método de direccionamiento de los PE y las facilidades de interconexión entre los PE, podemos distinguir entre: Pipeline o Procesadores Vectoriales. Array Processors(generalmente actúa como coprocesador). Procesadores Asociativos. Los procesadores pipeline son dificiles de clasificar. Pueden ser considerados tanto como arquitecturas SIMD, MISD, MIMD. El concepto es poderoso; puede ser aplicado igualmente a algunas unidades pequeñas, tales como sumadores de punto flotante o a unidades grandes tales como un procesador que comparte completamente una memoria común o redes de microcomputadoras. El poder computacional es segmentado en estaciones consecutivas. Los procesos son descompuestos en subprocesos que tienen que pasar a través de cada estación o estado. Retornemos a nuestro problema de clasificación. Si consideramos que una única instrucción, por ejemplo una suma de punto flotante en dos etapas, trata simultaneamente diferentes items de datos, entonces nuestro procesador es de tipo SIMD; si consideramos instrucciones diferentes en etapas consecutivas trabajando sobre el mismo agregado(vector) de datos, entonces sería MISD, si combinamos las dos interpretaciones anteriores tenemos una arquitectura MIMD. Múltiple instrucción, múltiple dato(MIMD): implica varios procesadores que operan asincronicamente comunicados entre sí por una memoria común. Dos aspectos intervienen para distinguir entre distintos diseños de esta categoria: Acoplamiento o conexionado de unidades de procesadores y módulos de memoria. Homogeneidad de las unidades de procesamientos. En multiprocesadores MIMD rigidamente acoplado, el número de unidades de procesamiento es fijo y operan bajo la supervisión de un esquema de control estricto. Generalmente el controlador es una unidad de HW. Muchos de los multiprocesadores fuertemente acoplados controlados por HW son heterogeneos en el sentido que consisten de unidades funcionales especializadas supervisadas por una unidad de instrucción, la cual decodifica, busca operandos y despacha ordenes a las unidades funcionales. En los más recientes sistemas de multiprocesamiento homogeneo, no dedicados unicamente a los computos rápidos de sistemas científicos, vemos conexiones más flexibles y modulares.
42
Pipeline El pipeline ofrece una manera económica de realizar paralelismo temporal en una computadora digital. Para lograr el pipelining se deberán subdividir los procesos de entrada en una secuencia de subprocesos cada una de las cuales puede ser ejecutada por una etapa de HW especializado que opera concurrentemente con otras etapas en el pipeline. Sucesivas tareas se ponen en el pipe y son ejecutadas de forma sobreladapa a nivel de los subprocesos. El procesamiento pipeline mejora de gran forma el throughput del sistema en una computadora digital. Principios de pipeline lineal(escalar): idealmente las distintas etapas del pipeline lineal tendrían igual velocidad de procesamiento. De otra manera, la etapa más lenta actúa como cuello de botella del pipe entero. Este cuello de botella, más la congestión causada por buffering impropio resultaría en muchas etapas ociosas esperando por subprocesos. La subdivisión de los procesos de entrada en una secuencia apropiada de subprocesos se convierte en un factor crucial en la determinación de la perfomance del pipeline. La relación de precedencia de un conjunto de subprocesos {T1,T2,....,Tk} para una dada tarea T, implica que alguna tarea Tj no podrá comenzar hasta que alguna tarea anterior Ti(i<j) finalice. Un procesador pipeline lineal básico podría ser: L
Input
Clock L: Latch
L
S1
L
S2
L
.................
L
Sk
Output
Si es la i-esima etapa
El pipeline consiste de una cascada de etapas de procesamiento. Las etapas son circuitos combinacionales que hacen operaciones aritméticas lógicas sobre los datos que fluyen a través del pipe. Las etapas están separadas por interfaces de alta velocidad llamados latches. Los latches son registros rápidos para almacenar resultados intermedios entre etapas. La información que fluye entre etapas está bajo el control de un reloj común aplicado a todos los latches simultaneamente. Período de reloj: en cada etapa Si hay un delay denotado por i . Sea l el tiempo de delay en cada interface latch. El período de reloj de un pipeline lineal está definido por T = max{i}k1 + 1 = m + 1 La recíproca del período de reloj es llamada frecuencia f = 1/ de un procesador pipeline. Supongamos tener un pipeline lineal de 4 etapas. Una vez que el pipeline se llenó se tendría una salida por período de reloj independientemente del número de etapas en el pipe. Idealmente, un pipeline lineal con k etapas puede procesar n tareas en Tk = k + (n-1) períodos de reloj, donde k ciclos son usados para llenar el pipeline o para completar la ejecución de la primera tarea y n-1 ciclos son necesarios para completar las restantes n-1 tareas. El mismo número de tareas en un procesador no-pipeline con una función equivalente en Tl = n.k de delay. Aceleración: definimos la aceleración de un procesador pipeline lineal sobre un procesador equivalente sin pipeline como: Sk = T1 / Tk = n.k / k + (n-1)
43
La máxima aceleración que un pipeline lineal puede proporcionar es k, donde k es el número de etapas del pipe. Esta velocidad máxima nunca es totalmente alcanzada a causa de la dependencia de datos entre instrucciones, interrupciones, branches del programa y otros factores. Muchos ciclos de pipeline se pueden perder en un estado de espera causado por ejecución de instrucciones fuera de secuencia. Supongamos tener un sumador pipeline que está linealmente construido con 4 etapas funcionales(S1,S2,S3,S4) y los delay de cada etapa son: 1 = 60 ns, 2 = 50 ns, 3 = 90 ns, 4 = 80 ns y el interface latch tiene un delay de l = 10 ns. El tiempo de ciclo de este pipeline será = 90 + 10 = 100 ns. Esto significa que la frecuencia de reloj del pipeline f = 1/ =1/100 = 10 MHZ. Si se tiene un sumador implementado sin pipeline, el tiempo total de delay será 1 + 2 + 3 +4 = 300 ns. En este caso la aceleración del sumador será 300/100 = 3 sobre el diseño del sumador no pipeline. Si pueden alcamzarse delays uniformes durante las cuatro etapas, digamos 75 ns por etapa(incluyendo el latch delay), entonces se puede lograr la aceleración máxima de 300/75 = 4.
La CPU de una computadora digital moderna puede ser particionada en 3 secciones: la unidad de instrucción, la cola de instrucciones y la unidad de ejecución. Desde el punto de vista operacional, las tres unidades tienen una estructura pipeline como muestra la siguiente figura:
Memoria Principal (multiway interleaved)
Jeraquía de Memoria
Cache
Unidad de Instrucción (Instruc. I+K+1) (I unit)
Etapas Pipeline: Actualizar PC y chequear interrupciones, instruction fetch, instruction decode, cálculo de dirección de operandos, Operand fetch
(Instruc. I+K) CPU Con Pipeline
(Instruc. I+2) (Instruc. I+1)
Unidad de ejecución (Instruc. I) (E unit)
FIFO cola de instrucciones(lista para ejecutar)
Pipelines aritméticos y lógicos
Programas y datos residen en memoria principal, los cuales usualmente consisten de módulos de memoria interleaved. La cache es un almacenamiento rápido de copias de programas y datos listos para la ejecución. La cache es utilizada para achicar el gap de velocidad existente entre memoria principal y CPU. La unidad de instrucción consiste de etapas pipeline para búsqueda de instrucciones, decodificación de instrucciones, calculo de dirección de operandos y busqueda de operandos(si es necesario). La cola de instrucciones es una cola FIFO, área de almacenamiento para instrucciones decodificadas y operandos encontrados. La unidad de ejecución puede contener múltiples pipelines funcionales para funciones aritméticas lógicas. Mientras la unidad de instrucción está
44
buscando la instrucción I+K+1, la cola de instrucciones almacena las instrucciones I+1, I+2,......,I+K y la unidad de ejecución ejecuta la instrucción I. En este sentido, el CPU es un buen ejemplo de pipeline lineal. Eficiencia: sean n, k, el número de tareas(instrucciones), el número de etapas pipeline y el período de reloj de un pipeline lineal, respectivamente. La eficiencia del pipeline está definida por: =
n.k. = k.[k. +(n-1) ]
n k+(n-1)
Note que cuando 1 entonces n. Esto implica que mientras mayor sea el número de tareas fluyendo a través del pipeline, mayor será la eficiencia. Throughput: es el número de resultados(tareas) que se pueden completar mediante un pipeline por unidad de tiempo. Refleja el poder computacional del pipeline. En terminos de la eficiencia y el período de reloj de un pipeline lineal, definimos el throughput como sigue: W=
n k.+(n-1)
=
donde n es el número total de tareas que están siendo procesadas durante el período de observación k.+(n-1) . En el caso ideal w = 1/ = f cuando 1. Esto significa que el máximo throughput de un pipeline lineal es igual a su frecuencia, la cual corresponde a un resultado de salida por período de reloj. Clasificación de Procesadores Pipeline. De acuerdo a su uso o niveles de procesamiento se tiene el siguiente esquema de clasificación: Pipelining Aritmético: las unidades aritméticas lógicas(ALUs) pueden ser segmentadas por operaciones pipeline en varios formatos de datos.(VER EJEMPLOS DE MULTIPLICADORES). Pipelining a Nivel Instrucción: la ejecución de un stream de instrucciones puede implementarse mediante pipeline sobrelapando la ejecución de la instrucción corriente, con la búsqueda, decodificación y búsqueda de operandos de instrucciones subsecuentes. Esta técnica es tambien llamada lookahead de instrucciones. Casi todas las computadoras actuales estan equipadas con pipelines para ejecución de instrucciones. Pipelining processor: se refiere al procesamiento pipeline del mismo stream de datos mediante una cascada de procesadores, cada uno de los cuales tiene una tarea específica. El stream de datos pasa el primer procesador con los resultados almacenados en un bloque de memoria, el cual es tambien accesible por el segundo procesador. El segundo procesador pasa los resultados refinados al tercero, y así siguiendo. El pipeline de multiples procesadores no está todavía muy aceptado como una práctica común. De acuerdo a su configuración y estrategias de control se tiene el siguiente esquema de clasificación: Pipelines Unifunción Vs. Multifunción: una unidad pipeline con una función fija y dedicada, tal como un sumador en punto flotante, se denomina unifuncional. Un pipe multifunción haría diferentes funciones en diferentes tiempos o en el mismo tiempo, mediante la interconexión de diferentes subconjuntos de etapas en el pipeline. Pipelines Estáticos Vs. Dinámicos: un pipeline estático asumiría solo una configuración funcional a la vez. Pipelines estáticos pueden ser o bien unifuncionales o multifuncionales. El pipelining es posible de hacer en pipes estáticos solo si instrucciones del mismo tipo son ejecutadas continuamente. La función hecha por un pipeline estático no cambiaría muy frecuentemente. De otra manera su perfomance sería muy baja. Un procesador pipeline dinámico permite que algunas configuraciones funcionales existan simultaneamente. En este sentido el pipeline dinámico deberá ser multifuncional. Por otro lado, un pipe unifuncional deberá ser estático. La configuración dinámica necesita control y mecanismos de secuenciamiento mucho más elaborado que los pipelines estáticos. La mayoría de las computadoras existentes están equipadas con pipes estáticos, ya sean unifuncionales o multifuncionales. Pipelines Escalares Vs Vectoriales: dependiendo de las instrucciones o tipos de datos, procesadores pipelines pueden ser tambien clasificados como pipelines escalares o vectoriales. Un pipeline escalar procesa una secuencia de operandos escalares bajo el control de un ciclo DO. Las instrucciones en un pequeño ciclo DO son 45
a menudo prebuscadas(prefetch) en un buffer de instrucción. Los operandos escalares requeridos para instrucciones escalares repetidas son movidas a una cache de datos en orden para continuamente suministrar el pipeline con operandos. Pipelines vectoriales son especialmente diseñados para manejar instrucciones vectoriales sobre operandos vectoriales. Computadoras que tienen instrucciones vectoriales son a menudo llamados procesadores vectoriales. El diseño de un pipeline vectorial es una expansión del pipeline escalar. El manejo de operandos vectoriales en pipelines vectoriales está bajo controles de firmware o HW(en lugar de control por SW como en el pipeline escalar).
Pipelines generales y Tablas de Reservación. En los pipelines lineales, las entradas y las salidas son totalmente independientes. En algunas computaciones, como recurrencias lineales, existen feedback connections(las entradas podrían depender de salidas previas). Los pipelines con feedback tendrían un flujo de dato no-lineal. Un uso inadecuado de entradas feedforward o feedback destruirían las ventajas inherentes del pipeline. Por otro lado el secuenciamiento con flujo de dato nolineal podría mejorar la eficiencia del pipeline. En la practica, muchos de los procesadores pipeline aritmeticos permiten conecciones no-lineales como un mecanismo para implementar recursión y funciones múltiples. Caracterizamos las estructuras de interconexión y los patrones de flujo de dato en pipelines generales con feedback o feedforward connections, además de la conexión en cascada de un pipeline lineal. Usamos un gráfico de dos dimensiones conocido como tabla de reservación para mostrar sucesivas etapas del pipeline son utilizadas en la evaluación de una función en sucesivos ciclos del pipeline. Las filas corresponden a las etapas del pipeline y las columnas a las unidades de tiempo de reloj. El número total de unidades de reloj en la tabla es llamado tiempo de evaluación para la función dada. Una tabla de reservación representa el flujo de dato a través del pipeline para una evaluación completa de una dada función. Un feedforward connection conecta una etapa Si a una etapa Sj tal que j i+2 y una feedback connection conecta una etapa Si a una etapa Sj tal que j i. En este sentido, un pipeline lineal “puro” es un pipeline sin feedback ni feedforward connections. Una entrada marcada en la posición (i,j) de la tabla indica que la etapa Si será usada j unidades de tiempo despues de la iniciación de la función de evaluación. Para un pipeline unifuncional, uno podría usar simplemente una “X” para marcar las entradas. Para un pipeline multifuncional, se usan diferentes marcas para diferentes funciones. Además diferentes funciones tendrían diferentes tiempos de evaluación. El patrón de flujo de dato en un pipeline estático, multifuncional puede ser totalmente descripto por una tabla de reservación. Un pipeline multifuncional podría usar diferentes tablas de reservación para diferentes funciones. Muchas características de la utilización del pipeline pueden ser reveladas por la tabla de reservación. Es posible tener múltiples marcas en una fila o columna. Múltiples marcas en una columna corresponden al uso simultáneo de múltiples etapas del pipeline. Marcas múltiples en una fila corresponden al uso repetido o uso prolongado de una etapa. Esta claro que un pipeline general podría tener múltiples caminos, uso paralelo de múltiples etapas, y flujo de dato no-lineal. Secuenciamiento y Prevención de Colisión. Una vez que una tarea se inicia en un pipeline estático, su patrón de flujo está fijo. Una iniciación se refiere al comienzo de una evaluación de función. Cuando dos o más iniciaciaciones intentan usar la misma etapa al mismo tiempo, resulta una colisión. El problema del secuenciamiento es planificar las tareas de forma tal de evitar colisiones y alcanzar un alto throughput. La tabla de reservación identifica el patrón de flujo espacio-tiempo de un dato completo a través del pipeline para una evaluación de función. En pipeline estático, todas las iniciaciones estan caracterizadas por la misma tabla de reservación. Por otro lado, iniciaciones sucesivas para pipeline dinámico estaría caracterizado por un conjunto de tablas de reservación, una por cada función que está siendo evaluada. El número de unidades de tiempo entre dos iniciaciones se llama latencia. Para un pipeline estático, la latencia es usualmente uno,dos o más. Sin embargo en pipelines dinámicos, entre diferentes funciones, se permiten latencia cero. La secuencia de latencias entre iniciaciones sucesivas se llama secuencia de latencia. Una secuencia de latencia que se repite se llama ciclo latencia. El procedimiento para elegir una secuencia de latencia se llama estrategia de control. Una estrategia de control que siempre minimiza la latencia entre la iniciación actual y la última se llama estrategia greedy. Una colision ocurre cuando dos tareas son iniciadas con una latencia igual a la distancia entre dos X’s en alguna fila de la tabla de reservación. El conjunto de distancias F= {l1, l2,......lr} entre todos los posibles pares de X’s sobre 46
cada fila de la tabla de reservación es llamado conjunto prohibido de latencias. Este conjunto contiene todas las posibles latencias que causan colision entre dos iniciaciones. El vector de colisión es un vector binario como el siguiente: (Cn,.........,C1) con Ci =
Ci = 1 si i es una latencia prohibida Ci = 0 si i es latencia permitida
Se podría usar un registro de desplazamiento de n-bit para almacenar el vector de colisión para la implementación de una estrategia de control para iniciaciones de tareas sucesivas en el pipeline. Cuando se inicia la primera tarea el vector de colisión es cargado paralelamente en el registro de desplazamiento como el estado inicial. El registro de desplazamiento es luego corrido un bit a derecha a la vez, agregando 0s en el extremo izquierdo. Una iniciación libre de colisión es permitida en el instante de tiempo t+k si, y solo si, un bit 0 está siendo corrido fuera del registro despues de k corrimientos desde el tiempo t. Faltan cosas!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Organización de un pipeline. Dentro de una unidad funcional, la misma estación puede ser usada varias veces durante la ejecuciónde una instrucción. Pero la planificación de la inicialización de las operaciones no es un problema trivial cuando se permiten bucles y cuando, además, deseamos sobrelapar varias instrucciones. En el ejemplo asumiremos que la única decisión de control es cuando`iniciar una nueva instrucción y que el pipeline está restringido a una única función. El objetivo del algoritmo de planificación es mantener un nivel máximo de iniciaciones mientras se evitan colisiones(dos o más subprocesos intentan usar la misma etapa al mismo tiempo). Usaremos latencia(intervalo de tiempo entre dos iniciaciones sucesivas) como una medida de perfomance. Ejemplo: una función pipeline con bucle puede ser la siguiente:
S1
S2
S3
S4
Esto puede ser reflejado en la tabla de reservación como sigue:
S1 S2 S3 S4
1 X
2
3
4
5 X
6
X
X X X
Las filas corresponden a las estaciones , las columnas a las unidades de tiempo, y una “X” en la fila i columna j indica que la estación i está ocupada en el tiempo j. Se ve que ocurrirá una colisión si dos instrucciones son inicializadas con una latencia igual a la distancia entre dos “X” en una fila dada. De la examinación de la tabla anterior, podemos obtener la lista de esas latencias prohibidas(en nuestro ejemplo la única latencia prohibida es 4, que corresponde a colisiones entre las estaciones S1 y S2). Con esta latencia prohibida podemos construir un vector de colisión: (Cn,.........,C1) con Ci = Ci = 1 si i es una latencia prohibida Ci = 0 si i es latencia permitida
47
En nuestro caso el vector de colisión es: (1,0,0,0). El vector de colisión puede ser interpretado como que una iniciación está permitida en cada unidad de tiempo donde Ci= 0. Así podemos comenzar construyendo un diagrama de estados finito de posibles iniciaciones.
Condiciones para implementar(hacer viable) un pipeline: 1. La evaluación de la función básica es equivalente a la evaluación secuencial de las subfunciones que la constituyen. 2. Las entradas de una subfunción provienen unicamente de las salidas de las subfunciones previas de la secuencia de operación. 3. Solo existe intercambio de E/S entre las subfunciones. 4. Disponer del HW apropiado para la evaluación de muchas subfunciones. 5. El tiempo requerido por las diferentes unidades sea aproximadamente el mismo.
Tiempo de set-up: es el tiempo que se le asocia a un pipeline para prepararlo para que empiece a operar. En el caso de un pipeline escalar(unifuncional) este tiempo va a ser inferior que en pipeline vectorial(multifuncional). En este último hay que hacer un trabajo previo para que el pipeline comience a funcionar, lo que consume un tiempo inicial. Tiempo de flush: es el tiempo de limpieza. Es el que se consume entre dos operaciones diferentes. El tiempo de flush se asocia principalmente con el pipeline vectorial(hay que realizar una serie de chequeos hasta tanto el pipeline quede liberado. Esto consume mucho tiempo). Uno de los requerimientos del pipeline es que es deseable que el tiempo entre las distintas etapas sea igual. Una etapa crítica es aquella en la que se obtienen los operandos y se almacenan. Para resolver este cuello de botella podemos disminuir el tiempo de estas etapas críticas por medio de dos recursos: interleaving de memoria o memoria cache. En general una etapa es usada más de uina vez por un dato, con lo cual, esta impone un condicionamiento al secuenciamiento del pipeline. Se busca lograr el regimen de iniciación má alto, pero evitando las condiciones de colisión, esto es, que má de un dato entre a competir por una etapa. Este problema se solucionaria con un procesamiento de vectores. El pipeline vectorial frente al escalar necesita más tiempo de set-up y un tiempo de flush. Por lo que el pipeline vectorial se va a justificar a partir de una dada longitud de vector. Comparación entre pipeline escalar y vectorial: los modelos anteriores(eficiencia, velocidad) no tienen en cuenta el tiempo de set-up y el tiempo de flush. Con estas dos consideraciones podemos expresar el tiempo de procesamiento vectorial como: Tv = Tvset-up + (L-1)Tvj + Tvflush Esto puede ser comparado con la operación de L operaciones en el modo escalar en el mismo pipe que puede ser expresado como: Ts = Tsset-up + (L-1)Tsj La diferencia entre Tvset-up y Tsset-up está en el seteo de registros adicionales. Pero estos registros aceleraron la captura de operandos así como los calculos de las direcciones efectivas, de manera tal que Tvj será más chico que Tsj . De las dos ecuaciones anteriores deducimos que el procesamiento vectorial será más ventajoso cuando: Tvset-up + (L-1)Tvj + Tvflush Ts = Tsset-up + (L-1)Tsj
48
Despejando L: L 1 + Tvset-up – Tsset-up + Tvflush Tsj - Tvj Esto visto graficamente:
Tvset-up + Tvflush Ventaja del pipeline vectorial Tsset-up
Pipeline a Nivel Instrucciones Pasos básicos en la ejecucion de una instrucción: todas las instrucciones pueden ser dividiadas en 5 pasos. Cada paso puede tomar 1 a más ciclos de reloj en el procesador. 1. Busqueda de instrucción: MAR PC; IR M[MAR} Operación: transladar el PC y buscar la instrucción desde memoria al Instruction Register. 2. Decodificación de instrucciones y búsqueda de registros: A Rs1; B Rs2; PC PC + 4 Operación: decodificar la instrucción y acceder al banco de registros para leer los registros. Tambien, incrementar el PC para que apunte a la próxima instrucción. 3. Ejecución / Dirección efectiva: la ALU opera sobre los operandos preparados en el paso anterior, haciendo una de tres funciones dependiendo el tipo de instrucción. Referencia a memoria: MAR A + (IR16)16##IR16..31; MDR Rd(valor que quiero almacenar en mem.) Operación: la ALU suma los operandos para formar la dirección efectiva y el MDR está cargado por un store. Instrucción ALU: ALUoutput A op (B or (IR16)16##IR16..31) Operación: la ALU hace la operación específicada por el opcode sobre el valor en A(Rs1) y sobre el valor en B o el signo extendido inmediato. Branch/Jump: ALUoutput PC + (IR16)16##IR16..31; cond (A op 0) Operación: la ALU suma el PC al valor del signo extendido inmediato(16 bit para branch y 26 para jump) para computar la dirección destino del branch. Para branches condicionales, un registro, el cual ha sido leido en el paso previo, es chequeado para decidir si esta dirección debe ser insertada en el PC. 4. Acceso a memoria / Completar branch: las únicas instrucciones activas en este paso son loads, stores, branchs y jumps. Referencia a memoria: MDR M[MAR] o M[MAR] MDR
49
Operación: acceso a memoria si es necesario. Si la instrucción es de carga, se retornan datos desde memoria; si es de almacenamiento, entonces los datos se escriben en memoria. En ambos casos la dirección usada es la única computada durante el paso anterior. Branch: if (cond) PC ALUoutput (branch) Operación: el PC es reemplazado con la dirección destino de branch. Para jumps la condición es siempre true. 5. Write-back: Rd ALUoutput o MDR Operación: escribe el resultado en el banco de registros desde memoria o desde la ALU. Pipelining es una técnica de implementación por la cual múltiples instrucciones son superpuestas en ejecución. Hoy en dia esta técnica es clave para hacer los procesadores más rápidos. Cada paso en el pipeline completa una parte de una instrucción. El trabajo que debe hacer una instrucción es dividido en partes pequeñas, cada una de las cuales toma una fracción de tiempo necesaria para completar la instrucción completa. Cada uno de estos pasos se llama etapa pipe o segmento pipe. Estas etapas están conectadas una tras otra formando el pipe- las instrucciones entran por un extremo, son procesadas a través de las distintas etapas, y salen por el otro extremo. El throughput del pipeline está determinado por cuan a menudo una instrucción sale del pipeline. Como las etapas del pipeline están enganchadas, todas las etapas deberán estar listas para proceder al mismo tiempo. El tiempo requerido para mover una instrucción un paso más es un ciclo máquina. La longitud de un ciclo máquina está determinado por el tiempo requerido por la etapa más lenta del pipe(porque todas las etapas deberán proceder al mismo tienmpo). A menudo el ciclo máquina es un ciclo de reloj(a veces dos, rara vez más). La meta del diseñador del pipeline es balancear la longitud de las etapas del pipeline. Si las etapas están perfectamente balanceadas, entonces el tiempo por instrucción sobre la máquina pipelined-asumiendo condiciones ideales(sin stalls)- es igual a: Tiempo por instrucción sobre una máquina no-pipelined Numero de etapas pipe Bajo estas condiciones, la aceleración del pipeline es igual al número de etapas del pipeline. Usualmente, sin embargo, las etapas no estan perfectamente balanceadas; además pipelining implica algún overhead. Pipelining da una reducción en el tiempo de ejecución promedio por instrucción. Esta reducción puede ser obtenida decrementando el tiempo de ciclo de reloj de una máquina pipelined o decrementando el número de ciclos de reloj por instrucción o ambas.
Pipeline básico:
Instrucción i Instrucción i+1 Instrucción i+2 Instrucción i+3 Instrucción i+4
1 IF
2 ID IF
3 EX ID IF
4 MEM EX ID IF
5 WB MEM EX ID IF
6
7
8
9
WB MEM WB EX MEM WB ID EX MEM WB
Aquí se tiene la condición ideal de un ciclo por instrucción(CPI). Si no hubiera tenido pipeline hubiera tenido 5 ciclos por instrucción. Pipelininig incrementa el throughput de instrucciones del CPU- el número de instrucciones completadas por unidad de tiempo- pero no reduce el tiempo de ejecución de una instrucción individual. En efecto, usualmente se incrementa levemente el tiempo de ejecución de cada instrucción debido al overhead en el control del pipeline. El incremento en el troughtput de instrucciones significa que un programa corre más rápido y baja el tiempo de ejecución total, aunque una instrucción en si misma no corra más rápido. El hecho de que el tiempo de ejecución de cada instrucción permanezca invariable pone límites en la profundidad del pipeline. Otras consideraciones de diseño limitan la frecuencia de reloj que puede alcanzarse profundizando el 50
pipeline. Por ejemplo el delay que se produce en cada latch. El clock skew tambien pone límites sobre el ciclo de reloj. La aceleración de un pipeline es: Tiempo promedio de instrucción sin pipeline Tiempo promedio de instrucción con pipeline
Problemas del pipeline: deberemos determinar que sucede en cada ciclo de reloj de la máquina y estar seguros de que el sobrelapamiento de instrucciones no sobrecompromete los recursos. Por ejemplo, una sola ALU no puede computar una dirección efectiva y hacer una resta al mismo tiempo. Las operaciones que ocurren durante la ejecución de la instrucción(que se detallaron antes) se modifican para ejecutarlas en un pipeline como se muestra a continuación: PC unit
Memoria
Data path
IF
PC PC + 4
IR Mem[PC]
ID
PC1 PC
IR1 IR
Etapa
A Rs1; B Rs2; DMARA + (IR16)16##IR16..31; O ALUoutputA op (B o (IR116)16##IR116..31); O ALUoutputPC1+(IR116)16##IR116..31); cond(Rs1 op 0); SMDRB;
EX
MEM
if (cond) PCALUoutput
LMDRMem[DMAR] O Mem[DMAR]SMDR
WB
ALUoutput1ALUoutput
RdALUoutput1 o LMDR
La figura muestra las principales unidades funcionales y que sucede en cada etapa del pipeline en cada unidad. En algunas de las etapas no todas las acciones listadas pueden ocurrir, porque ellas se aplican bajo diferentes asunciones acerca de la instrucción. Por ejemplo hay tres operaciones dentro de la ALU durante la etapa EX. La primera ocurre solamente sobre load o store; la segunda sobre operaciones de la ALU; la tercera ocurre solo sobre branches. Las variables ALUoutput1, PC! Y IR1 guardan valores para usar en etapas posteriores del pipeline. Probablemente el mayor impacto del pipelining sobre los recursos de la máquina está en la memoria. Aunque el tiempo de acceso a memoria no cambia, el ancho de banda de memoria pico se incrementa en 5 veces sobre la máquina sin pipeline puesto que se requieren 2 accesos a memoria en cada reloj en la máquina con pipeline versus 2 accesos cada 5 ciclos de reloj en una máquina sin pipeline con el mismo número de pasos por instrucciones. Para proporcionar 2 accesos a memoria en cada reloj, la mayoría de las máquinas usan cache separados para instrucciones y para datos.
51
El mayor obstáculo del Pipelining- Colisiones: existen situaciones, llamadas colisiones, que impiden que la próxima instrucción en el stream de instrucciones se ejecute en el ciclo de reloj designado. Colisiones reducen la perfomance de la condición ideal(1 ciclo por instrucción) del pipelining. Hay tres clases de colisiones: 1. Colisiones estructurales: surgen de los conflictos por recursos cuando el HW no puede soportar todas las posibles combinaciones de instrucciones en ejecución sobrelapada simultánea. 2. Colisiones de datos: surgen cuando una instrucción depende de los resultados de instrucciones previas en una manera que está expuesta por el sobrelapamiento de instrucciones en el pipeline. 3. Colisiones de control: surgen del pipelining de los branches y otras instrucciones que modifican el PC. Las colisiones pueden hacer necesario la implementación de stalls en el pipeline. Un stall a menudo requiere que algunas instrucciones pueden proceder, mientras que otras se detienen momentaneamente. Tipicamente, cuando una instrucción es demorada mediante stall, todas las instrucciones siguientes en el pipeline son tambien demoradas. Instrucciones anteriores al stall pueden continuar. Pero no pueden buscarse nuevas instrucciones durante el stall. Colisiones estructurales: cuando una máquina implementa pipeline, la ejecución sobrelapada de instrucciones requiere pipelinig de las unidades funcionales y duplicación de recursos para permitir todas las combinaciones posibles de instrucciones en el pipeline. Si algunas combinaciones de instrucciones no pueden acomodarse debido a los conflictos de recursos, se dice que la máquina tiene una colisión estructural. Las instancias más comunes de este tipo de colisión generalmente surgen cuando alguna unidad funcional no está totalmente pipelined. Entonces una secuencia de instrucciones que usan la misma unidad funcional no pueden ser iniciadas secuencialmente en el pipeline. Otra forma común de colisiones estructurales aparece cuando los recursos no han sido duplicado lo suficiente para permitir que todas las combinaciones de instrucciones en el pipeline se ejecuten. Por ejemplo, una máquina podría tener solo portico de escritura en un banco de registros, pero bajo ciertas circunstancias, el pipeline querría hacer dos escrituras en un ciclo de reloj. Esto generará una colisión estructural. Cuando una secuencia de instrucciones encuentra esta colisión el pipeline demorará(stall) una de las instrucciones hasta que la unidad requerida este disponible. Muchas máquinas pipeline comparten una sola memoria pipeline para datos e instrucciones. Como resultado, cuando una instrucción contiene una referencia a datos en memoria, el pipeline deberá hacer un stall por un ciclo de reloj; la máquina no puede buscar la próxima instrucción porque la referencia al dato está usando el portico de memoria. Si todos los otros factores son iguales, una máquina sin colisiones estructurales siempre tendrá un CPI bajo. Porqué, entonces, el diseñador permite colisiones estructurales? Existen dos razones: para reducir el costo(ancho de banda de memoria y mucho consumo de compuertas para unidades pipelined) y la latencia(pocos clocks por operación) de la unidad. Hacer pipeline a todas las unidades funcionales tambien puede ser costoso. Si la colisión estructural no ocurre muy a menudo, quizás no valga la pena el costo de evitarlo. Es a menudo posible diseñar unidades sin pipeline, o con pipeline parcial con un delay total más corto que una unidad con pipeline total. Colisiones de datos: ocurre cuando el orden de acceso a los operandos es cambiado por el pipeline versus el orden normal encontrado por la ejecución secuencial de las instrucciones. Consideremos la ejecución de estas instrucciones: ADD R1, R2, R3 SUB R4, R1, R5 La instrucción SUB tiene como origen, R1, que es el destino de la instrucción ADD. La instrucción ADD escribe el valor de R1 en la etapa WB, pero la instrucción SUB lee el valor durante la etapa ID. Este problema se denomina colisión de datos. A menos que se tomen precauciones para impedirlo, la instrucción SUB leerá un valor equivocado y tratará de usarlo. Este problema puede ser resuelto usando una simple técnica de HW llamada forwarding(tambien llamada bypassing). Esta técnica trabaja como sigue: el resultado de la ALU está siempre alimentando a los latches de entrada de la ALU. Si el HW de forwarding detecta que una operación previa de ALU ha escrito el registro correspondiente al origen de una operación actual, lógica de control selecciona el resultado avanzado(forwarded) como la entrada de la ALU en lugar de el valor leido del banco de registros. Notemos que con forwarding, si la 52
SUB está detenida(stalled), la ADD será completada, y el bypass no será activado, causando que el valor del registro sea usado. Esto tambien vale para el caso de una interrupción entre dos instrucciones. A continuación se muestra un conjunto de instrucciones en el pipeline que necesitan bypass. ADD R1, R2, R3 SUB
R4, R1, R5
IF
ID
EX
MEM WB
IF
ID
EX
MEM WB
IF
ID
EX
MEM WB
IF
ID
EX
MEM WB
IF
ID
EX
AND R6, R1, R7 OR
R8, R1, R7
XOR R10, R1, R11
MEM WB
La instrucción ADD setea R1, y 4 instrucciones que le siguen la usan. Se deberá hacer bypass del valor de R1 hacia las instrucciones SUB, AND y OR. En el tiempo que el XOR hace la lectura de R1 en la fase ID, la instrucción ADD ha completado WB, y el valor está disponible. Es deseable achicar el número de instrucciones a las que se debe aplicar bypass, puesto que cada nivel requiere HW especial. Teniendo presente que el banco de registros es accedido dos veces en un ciclo de reloj, es posible hacer que el registro escriba en la primera parte de la fase WB y la lectura en la segunda mitad de la fase ID. Esto elimina la necesidad de hacer bypass en la cuarta instrucción. Cada nivel de bypass requiere un latch y un par de comparadores para examinar si las instrucciones adyacentes comparten destino y origen.
Banco de Registros
Mux
Mux
Bus de escritura de resultados
Bypass Paths
ALU
R4
Buffers de resultado de ALU
R1
Aquí se muestra la estructura de la ALU y su unidad de bypass, como tambien que valores están en los registros de bypass para el conjunto de instrucciones del ejemplo anterior. Se necesitan dos buffers de resultado de ALU para almacenar resultados de la ALU que serán puestos en los registros destinos en las próximas dos fases WB. Para operaciones de la ALU, el resultado es siempre avanzado(forwarded) cuando la instrucción que usa el resultado como origen entra en su etapa EX. Los resultados en los buffers pueden ser entradas en algún port de la ALU, via un par de multiplexores. El control de multiplexor puede ser hecho o bien por la unidad de control o bien localmente mediante lógica asociada al bypass. En ambos casos la lógica deberá testear si alguna de las dos instrucciones previas escribieron un registro que es la entrada de la instrucción actual. Si es así, entonces el multiplexor selecciona del registro de resultado apropiado en lugar del bus. Como la ALU opera en una sola etapa 53
del pipeline, no existe la necesidad de un pipeline stall con cualquier combinación de instrucciones de ALU una vez que los bypasses han sido implementados. Una colisión se produce dondequiera que hay dependencias entre instrucciones, y ellas están lo bastante cercanas que el sobrelapamiento causado por el pipelining cambiaría el orden de acceso a los operandos. Nuestros ejemplos de colisiones han sido con operandos registros, pero es tambien posible para un par de instrucciones crear una dependencia mediante la escritura y lectura de la misma locación de memoria. En nuestro pipeline, sin embargo, referencias a memoria se mantiene siempre en orden, previniendo que aparezcan este tipo de colisiones. Misses en cache podrían causar referencias de memoria fuera de orden si permitimos que el procesador continue trabajando sobre instrucciones posteriores mientras que instrucciones anteriores que produjeron el miss en cache acceden a memoria. La solución sería hacer un stall del pipeline entero, haciendo que la instrucción que contiene el miss corra por múltiples ciclos de reloj. Colisiones de datos pueden clasificarse en tres tipos, dependiendo del orden de lectura y escritura de las instrucciones. Consideremos dos instrucciones i y j, con i ocurriendo antes que j. Las posibles colisiones de datos son: RAW(read after write): j trata de leer un origen antes de que i lo escriba, entonces j toma incorrectamente el valor viejo. WAR(write after read): j trata de escribir su destino antes que sea leido por i, entonces i incorrectamente toma un nuevo valor. WAW(write after write): j trata de escribir un operando antes de que sea escrito por i. La escritura se hace en orden equivocado, dejendo el valor escrito por i en lugar del valor escrito por j.
No todas las colisiones de datos pueden ser manejadas sin que se afecte la perfomance. Consideremos la siguiente secuencia de instrucciones: LW R1, 32(R6) ADD R4, R1, R7 SUB R5, R1, R8 AND R6, R1, R7 Este caso es diferente a los anteriores. La instrucción LW no tiene el dato hasta el fin del ciclo MEM, mientras que la instrucción ADD necesita tener el dato al comienzo de dicho ciclo de reloj. Esto es, la colisión de datos producida por usar el resultado de una instrucción de carga no puede ser completamente eliminado mediante simple HW. La instrucción de carga tiene un delay o latencia que no puede ser eliminada mediante forwarding solamente- para hacer esto se requeriría que el tiempo de acceso a datos sea 0. La solución más común es agregar HW llamado pipeline interlock. En general, pipeline interlock detecta una colisión y detiene(stall) el pipeline hasta que se libra la colisión. En este caso, el interlock detiene el comienzo del pipeline con la instrucción que quiere usar el dato hasta que se produzca el dato que se quiere usar. Este ciclo de retardo, llamado pipeline stall, permite que la carga del dato arribe de memoria; luego si se puede adelantar(forwarded) mediante HW. El CPI para la instrucción detenida se incrementa por la longitud del stall. El proceso donde una instrucción se mueve desde la etapa de decodificación(ID) a la etapa de ejecución(EX) de este pipeline se conoce como despacho de instrucción(instruction issue); y la instrucción que hizo este paso se dice despachada(issued). Si la colisión de dato existe, la instrucción es retardada(stalled) antes de que sea despachada. En lugar de hacer que el pipeline se detenga otra alternativa sería que el compilador tratara de planificar el pipeline para evitar estos stalls, reacomodando la secuencia de código para eliminar las colisiones. Por ejemplo, el compilador trataría de evitar generar código con una carga seguida por un uso inmediato del registro destino de carga. Esta técnica se llama planificación de pipeline o planificación de instrucciones. Colisiones de Control: pueden causar una mayor perdida de perfomance que las colisiones de datos. Cuando un branch es ejecutado, este podría o no cambiar el PC a otra dirección distinta de PC+4(instrucción siguiente al branch). Recordemos que si el branch cambia el PC a su dirección destino, se dice que es un branch tomado; si no 54
es así se dice que es un branch no tomado. Si la instrucción i es un branch tomado, entonces el PC normalmente no cambia hasta el final de la fase MEM, despues de completar el cálculo de direcciones y comparaciones. Esto significa que deberá hacerse stall durante 3 ciclos de reloj, al final de los cuales el nuevo PC es conocido y la instrucción apropiada puede ser buscada. Este efecto es llamado colisión de control. A continuación se muestra un stall de tres ciclos para colisión de control: Instrucción Branch Instrucción i+1 Instrucción i+2 Instrucción i+3 Instrucción i+4 Instrucción i+5 Instrucción i+6
IF
ID stall
EX stall stall
MEM stall stall stall
WB IF stall stall stall
ID IF stall stall stall
EX ID IF stall stall stall
MEM EX ID IF stall stall
WB MEM EX ID IF stall
WB MEM EX ID IF
WB MEM EX ID
Pero este pipeline no es posible porque no conocemos que la instrucción es un branch hasta despues del fetch de la próxima instrucción. La solución es rehacer el fetch una vez que el destino es conocido.
Instrucción Branch Instrucción i+1 Instrucción i+2 Instrucción i+3 Instrucción i+4 Instrucción i+5 Instrucción i+6
IF
ID IF
EX stall stall
MEM stall stall stall
WB IF stall stall stall
ID IF stall stall stall
EX ID IF stall stall stall
MEM EX ID IF stall stall
WB MEM EX ID IF stall
WB MEM EX ID IF
WB MEM EX ID
Tres ciclos de reloj perdidos por cada branch es una perdida significante. Con un 30% de frecuencia de branch y un CPI ideal de 1, la máquina con branch stalls logra solo la mitad de la aceleración ideal del pipeline. Esto es, reducir el branch penalty es una cuestión crítica. El número de ciclos de reloj en un branch stall puede ser reducido en dos pasos: 1. Buscar si el branch es tomado o no lo antes posible en el pipeline. 2. Computar el PC tomado(dirección destino del branch) lo antes posible. Para optimizar el comportamiento del branch, ambos pasos deben ser hechos- ya que no ayuda conocer el destino del branch sin conocer si la próxima instrucción a ejecutar es el destino o PC+4. Evaluar al condición del branch puede realizarse al final del ciclo ID usando lógica especial para este test. Para tomar ventaja de una decisión previa sobre si el branch es tomado, ambos PCs(tomado y no tomado) deberán ser computados previamente. Computar la dirección detsino del branch requiere un sumador separado, el cual puede sumar durante la fase ID. De esta forma habrá solamente un stall de un ciclo de reloj. En algunas máquinas las colisiones de control son aún más costosas en ciclos de reloj que en nuestro ejemplo puesto que el tiempo en evaluar la condición del branch y computar el destino puede ser mayor. Por ejemplo, en máquinas con fases de decodificación búsqueda de registros separada probablemente se tendrá un branch delay- la longitud de la colisión de control- que es al menos un ciclo de reloj más largo. Reducir Branch Penalties: existen alternativas estáticas(SW) y dinámicas(HW). Entre las estáticas estan: stall, predecir tomado, predecir no tomado y branch retardado. En los esquemas estáticos las predicciones son adivinadas en tiempo de compilación. Hay esquemas más ambiciosos que usan HW para predecir branches dinámicamente. El esquema más simple es congelar el pipeline, guardando cualquier instrucción despues del branch hasta que se conozca el destino del branch. Es la solución más simple y fue la usada en los ejemplos anteriores(stalls). Una mejor solución, aunque un poco más complicada es pronosticar el branch como no tomado, simplemente permitiendo al HW que continue como si el branch no fuera ejecutado. Se deberá tener cuidado de no cambiar el estado de la máquina hasta que el branch es definitivamente conocido. La complejidad que surge de esto podría llevarnos a reconsiderar la solución más simple. 55
Un esquema alternativo es pronosticar el branch como tomado. Tan pronto como el branch es decodificado y la dirección destino es computada, asumimos el branch como tomado y comenzamos buscando y ejecutando en el destino. Como en el pipeline que hemos estado viendo no conocemos la dirección destino mucho antes que el branch resultado, no hay ventaja en este approach. Sin embargo, en algunas máquinas- especialmente aquellas con código de condición o condiciones de branch más poderosas- que el destino del branch sea conocido antes que el branch resultado, puede ser muy útil. Branch No-Tomado Instrucción i+1 Instrucción i+2 Instrucción i+3 Instrucción i+4
IF
Branch Tomado Instrucción i+1 Instrucción i+2 Instrucción i+3 Instrucción i+4
IF
ID IF
EX ID IF
MEM EX ID IF
WB MEM EX ID IF
WB MEM WB EX MEM WB ID EX MEM WB
ID IF
EX IF stall
MEM ID IF stall
WB EX ID IF stall
MEM EX ID IF
WB MEM WB EX MEM WB ID EX MEM WB
Algunas máquinas usan otras técnicas llamadas branchs retardados, los cuales han sido usados en muchas unidades de control microprogramadas. En un branch retardado, el ciclo de ejecución con un branch delay de longitud n es: Branch Instruction Secuential Successor1 Secuential Successor2 ......................................
Secuential Successorn Branch Target if taken Los sucesores secuenciales están en los branches delay slots. Las instrucciones sucesoras deben ser válidas y útiles. Tres formas en las que el branch delay puede ser planificado son las siguientes: a) From Before ADD R1, R2, R3 If R2=0 then Delay slot
Se hace If R2=0 then ADD R1, R2, R3
a) From Target 56
SUB R4, R5, R6 ADD R1, R2, R3 If R1=0 then Delay slot Se hace
ADD R1, R2, R3 If R1=0 then SUB R4, R5, R6
b) From fall through ADD R1, R2, R3 If R1=0 then Delay slot SUB R4, R5, R6
Se hace
ADD R1, R2, R3 If R1=0 then SUB R4, R5, R6
La parte de arriba de los cuadros muestra el código antes de planificar, y la parte de abajo muestra el código planificado. En a) el delay slot está planificado con una instrucción independiente desde antes(from before) del branch. Esta es la mejor elección. Las estrategias b) y c) son usadas cuando a) no es posible. En las secuencias de código para b) y c) el uso de R1 en la condición del branch impide que la instrucción ADD(cuyo destino es R1) sea movida despues del branch. En b) el branch-delay slot está planificado desde el destino(from target); usualmente la instrucción destino necesitará ser copiada porque esta puede ser alcanzada por otro camino. La estrategia b) se prefiere cuando el branch es tomado con alta probabilidad, tal como un loop-branch. Finalmente el branch podría ser planificado desde el fall through, como en c).
57
Estrategia de Planificación
Requerimientos
Cuando mejora la perfomance?
a) From before branch
El branch no deberá depender de instrucciones reordenadas.
Siempre
b)From target
Estará OK para ejecutar instrucciones Cuando el branch es tomado. reordenadas si el branch es no-tomado. Podría alargar el programa si se Podría necesitar duplicar instruciones. duplican instrucciones.
c) From Fall Through
Estará OK para ejecutar instrucciones si el branch es tomado.
Cuando el branch es no tomado.
Las limitaciones primarias de la planificación del branch retardado surgen de las restricciones sobre las instrucciones que son planificadas dentro del delay-slot y de nuestra capacidad para pronosticar en tiempo de compilación si un branch es tomado o no. Existe un costo de HW adicional para los branches retardados. Se necesitan múltiples PCs para restaurar correctamente el estado cuando ocurre una interrupción. Por ejemplo si ocurre una interrupción cuando se completa una instrucción branch tomado, pero antes que se completen todas las instrucciones en el delay slot y el branch target. En este caso, el PC’s de los delay slots y el PC del branch target deberán salvarse, porque ellos no son secuenciales.
Reducción de branch penalty con Dynamic Hardware Prediction. Ahora veremos como usar HW para predecir dinamicamente el resultado de un branch- la predicción cambiará si el branch cambia su comportamiento mientras el programa está corriendo. El esquema más simple es el branch-prediction buffer. Un branch-prediction buffer es una pequeña memoria indexada por la parte más baja de la dirección de la instrucción branch. La memoria contiene un bit que indica si el branch fue recientemente tomado o no. Esta es la clase más simple de buffer; no tiene tags y solo es útil para reducir el branch delay cuando este es mayor que el tiempo que lleva computar los posibles PCs destino. No sabemos, en efecto, si la predicción es correcta. Si se asume como correcta, el fetching comienza en la dirección predicha. Si la predicción del branch fue equivocada, se invierte el bit de predicción. Este esquema de predicción de un bit tiene un problema: Si un branch es casi siempre tomado, entonces cuando este no sea tomado, se predice incorrectamente dos veces en lugar de una(ejemplo del loop). Para remediar esto se usan esquemas de predicción de dos bits. ¿??????????
Jerarquía de Memoria Principio de localidad: para todo programa una porción de su espacio direccionado en cualquier instante de tiempo, tiene dos dimensiones: Localidad Temporal(localidad en tiempo): si un item es referenciado, este tiende a ser referenciado nuevamente en el futuro inmediato. Por ejemplo bucles, variables temporales. Localidad Espacial(localidad en espacio): si un item es referenciado, items cercanos tienden a ser referenciados en un futuro inmediato. Por ejemplo arreglos.
58
Una jerarquía de memoria es una reacción natural a localidad y tecnología. El principio de localidad y las directivas que un HW pequeño es más rápido produce el concepto de una jerarquía basada en diferentes velocidades y tamaños. Como la memoria más lenta es barata, una jerarquía de memoria está organizada en algunos niveles- cada una más pequeña, más rápida y más costosa que la del nivel inferior. Se puede utilizar el principio de localidad para mejorar la perfomance. La estrategia será mapear direcciones de una memoria con mayor capacidad a una memoria pequeña pero más rápida. Componentes de la Jerarquía de Memorias Objetivo: Tratar de emparejar los tiempos de acceso a memoria con los tiempos del PC. Obtener una perfomance adecuada a un costo razonable, o sea alcanzar la perfomance del nivel más bajo a un costo dado basicamente por el nivel más alto. El criterio de velocidad es el siguiente: obtener una relación entre la frecuencia de accesos de las unidades de información con su lugar en la jerarquía. El costo puede ser descompuesto en dos factores: un factor estructural, que limita el tamaño de los componentes más rapidos y por lo tanto más costosos; y un factor dinámico impuesto por el manejo de transferencia entre niveles, las que consumen tiempo y requieren el uso de recursos apartes de la memoria(por ejemplo canales, sistemas operativos). Desde un punto de vista estructural una jerarquía de memoria consiste de L niveles. A cada nivel i, la memoria M i tiene componentes caracterizados por tamaño o capacidad, tiempo promedio de acceso y costo por bit. Un nivel puede contener varios módulos no necesariamente identicos, pero relacionados en función de costo, capacidad, velocidad. Las conexiones entre los módulos de distintos niveles están caracterizados por la velocidad de transferencia o ancho de banda y por la cantidad física de información transferida.
PC
L0 (cache) L1 (memoria principal) L2 (memoria secundaria) L3 (archivos On-Line) L4 (archivos Off-Line)
Principios Generales de Jerarquía de Memoria: una jerarquía de memoria generalmente consiste de muchos niveles, pero se maneja entre dos niveles adyacentes a la vez.. El upper level -el más cercano al procesador- es más pequeño y rápido que el lower level. La unidad mínima de información que puede estar o bien presente o no presente en la jerarquía de dos niveles es llamada bloque. El tamaño del bloque puede ser fijo o variable. Si es fijo, el tamaño de la memoria es un múltiplo del tamaño de ese bloque. 59
Procesador
Upper Level
Bloques
Lower Level
Los exitos o fracasos de un acceso al upper level es designado como un hit o un miss: Un hit es un acceso a memoria encontrado en el upper level, mientras que un miss significa que no se ha encontrado en ese nivel. Hit rate es el porcentaje de accesos a memoria que se resuelven con éxito en el upper level. Miss rate es el porcentaje de accesos a memoria no encontrados en el upper level(1.0 – hit rate). Como la perfomance es la mayor razón para tener una jerarquía de memoria, la velocidad de los hits y misses son importante. Hit time es el tiempo para acceder al upper level de la jerarquía de memoria, se incluye el tiempo en determinar si el acceso es un hit o un miss. Miss penalty es el tiempo para reemplazar un bloque en el upper level por el correspondiente bloque del lower level, más el tiempo en entregar este bloque al dispositivo que lo requiere(normalmente CPU). El miss penalty está dividido en dos componentes: access time- el tiempo en acceder a la primera palabra de un bloque en un miss; y transfer time- el tiempo adicional para transferir las palabras restantes en el bloque. El tiempo de acceso está relacionado a la latencia de la memoria de lower level, mientras que el transfer time está relacionado al ancho de banda entre las memorias lower level y upper level. La dirección de memoria está dividida en partes que acceden cada parte de la jerarquía. La block-frame address es la parte de mayor orden de la dirección que identifica un bloque en este nivel de la jerarquía. El block-offset address es la parte de orden más bajo de la dirección e identifica un item dentro del bloque. El tamaño del blockoffset address es log2(tamaño de bloque); el tamaño del block-frame address es el tamaño de la dirección total en este nivel menos el tamaño del block-offset address. Evaluación de Perfomance de una Jerarquía de Memoria. Una medida para evaluar la perfomance de una jerarquía de memoria es el tiempo promedio para acceder a memoria: Tiempo Promedio de Acceso a Memoria = Hit time + Miss rate * Miss penalty (es expresado en ciclos o en nanosegundos)
60
La relación de el tamaño de un bloque para miss penalty y miss rate se muestra a continuación:
Miss Penalty
Transfer time
Miss rate
Access time
Tamaño de bloque
Tamaño de bloque
Esta representación asume que el tamaño de la memoria de upper level no cambia. La porción de tiempo de acceso del miss penalty no es afectada por el tamaño de bloque, pero el tiempo de tranferencia se incrementa con el tamaño del bloque. Si el tiempo de acceso es grande, inicialmente existirá un pequeño miss penalty adicional relativo al tiempo de acceso cuando el tamaño de bloque se incrementa. Sin embargo, aumentar el tamaño de bloque significa que pocos bloques podrán estar en la memoria de upper level. Incrementar el tamaño de bloque baja el miss rate hasta que, como gran parte del bloque no es usado se desplaza la información útil en el upper level, y los miss rate comienzan a aumentar. Si hago bloques muy grandes comprometo la localidad temporal. Por ejemplo si la cache es de un bloque, transferir un bloque desde memoria principal obliga a sacar el bloque que actualmente está en cache. La meta de la jerarquía de memoria es reducir el tiempo de ejecución, no los misses. Por lo tanto los diseñadores de computadoras prefieren un tamaño de bloque cuyo tiempo de acceso promedio sea bajo en lugar de un miss rate que sea bajo.
Tiempo de Acceso promedio
Tamaño de bloque Procesadores diseñados sin jerarquía de memoria son simples porque los accesos a memoria siempre toman la misma cantidad de tiempo. Los misses en una jerarquía de memoria significan que el CPU deberá ser capaz de manejar tiempos variables de accesso a memoria. Si el miss penalty está en el orden de los 10 ciclos de reloj, el procesador espera que la transferencia de memoria se complete(por HW). Por otro lado, si el miss penalty necesita cientos de ciclos de reloj del procesador es demasiado costoso dejar al procesador ocioso; en este caso el CPU es interrumpido, se produce un cambio de contexto y es usado para otro proceso durante el manejo del miss(por SW, pero la dtección es por HW). El procesador deberá tener un mecanismo para determinar si la información está o no en el nivel más alto de la jerarquía de memoria. Este chequeo ocurre en cada acceso a memoria y afecta el hit time; para tener una perfomance aceptable este chequeo debe ser implementado en HW. Una cuestión final de la jerarquía de memoria es que la computadora deberá tener un mecanismo para transferir bloques entre el upper level y lower level. Si la transferencia de bloque toma decenas de ciclos de reloj, se controla por HW; si toma cientos de ciclos de reloj, puede ser controlado por SW.
61
4 cuestiones para la clasificación de jerarquías de memoria: Q1: Donde puede ser puesto un bloque en el upper level? (ubicación de bloque) Q2: Como se encuentra un bloque si está en el upper level? (identificación de bloque) Q3: Cual bloque se reeemplaza cuando ocurre un miss? (reemplazo de bloque) Q4: Que sucede en una escritura? (codifica la escritura) Caches Cache es el nombre elegido para representar el nivel de la jerarquía de memoria entre CPU y memoria principal. Su tamaño generalmente está entre 1-256 Kb. Q1: Donde puede ser puesto un bloque en el upper level? Las restricciones sobre donde puede ubicarse un bloque crea tres categorias de organización de cache: Mapeo Directo: Si cada bloque de memoria principal aparece en un único lugar de la cache.El mapeo es usualmente (block-frame address) modulo (número de bloques en la cache). Mapeo Full Asociativo: Si un bloque puede ser puesto en cualquier lugar de la cache. Mapeo Set Asociativo: si un bloque puede ser puesto en un conjuto restricto de lugares en la cache. Un conjunto es un grupo de 2 o más bloques en la cache. Un bloque primero es mapeado en un conjunto, y entonces el bloque puede ser puesto en cualquier lugar dentro del conjunto. El conjunto es usualmente elegido por el bit de selección; esto es (block frame address) modulo (número de conjuntos en la cache). Si hay n bloques en un conjunto, la ubicación en la cache se denomina n-way set asociativo. Mapeo directo puede ser visto como one-way set asociativo y mapeo full asociativo con m bloques puede ser llamado m-way asociativo set asociativo. Q2: Como se encuentra un bloque si está en la cache? Las caches incluyen un tag de dirección sobre cada bloque que da la block-frame address. El tag de cada bloque de cache que podría contener la información deseada es chequeada para ver si hace match con la block-frame address de la CPU.
Bloque
Full asociativo 0 1 2 3 4 5 6 7
Mapeo Directo 0 1 2 3 4 5 6 7
Set Asociativo 0 1 2 3 4 5 6 7
Data Set 0 Set 1 Set 2 Set 3
Tag
1 2
1 2
1 2
En la ubicación full asociativa el bloque para la block-frame address 12 puede aparecer en cualquiera de los 8 bloques, esto es, se deben examinar 8 tags. En ubicación mapeo directo existe solo un bloque de cache donde el bloque de memoria 12 puede ser encontrado. En ubicación set asociativa, con 4 conjuntos, el bloque de memoria 12 estará en el set 0(12 mod 4), esto es, se chequean los tags de los bloques de cache 0 y 1. Para obtener nayor velocidad en accesos a cache la búsqueda deberá hacerse en paralelo para mapeos full asociativo y set asociativo.
62
Deberá haber una forma de saber si un bloque de cache no tiene infornación válida. El procedimiento más común es agregar un bit de válido al tag para decir si o no esta entrada contiene una dirección válida. Si el bit no está seteado, no puede hacerse match sobre la dirección. El tag requerido para cada bloque tiene un costo. Una ventaja de incrementar los tamaños de bloque es que el overhead producido por el tag en cada entrada de cache se convierte en una pequeña fracción del costo total dela cache. Una dirección se divide en tres campos para encontrar datos en una cache set asociativo: el campo block offset usado para seleccionar el dato deseado del bloque, el campo índice usado para seleccionar el set y el campo tag usado para la comparación. Mientras que la comparación podría ser hecha sobre más de una dirección que el tag, no existe la necesidad de: Chequear el índice sería redundante, puesto que fue usado para seleccionar el conjunto a ser chequeado(por ejemplo una dirección almacenada en el set 0, deberá tener 0 en el campo índice o no podría estar almacenado en el conjunto 0). El offset es innecesario en la comparación porque todos los offsets de bloque hacen match y el bloque entero está presente o no. Si el tamaño total se mantiene igual, el incremento de asociatividad incrementa el número de bloques por set, decrementando el tamaño del índice e incrementando el tamaño del tag. Q3: Que bloque se reeemplaza cuando ocurre un miss? Si la elección fuera entre un bloque que tiene datos válidos y uno que no, es facil seleccionar que bloque reemplazar. Veremos que decisión se toma entre bloques que contienen datos válidos. Un beneficio de mapeo directo es que las decisiones de HW se simplifican. En efecto, es tan simple porque no hay elección: solo un bloque se chequea por un hit, y solo ese bloque puede ser reemplazado. Con full asociativo o set asociativo, hay algunos bloques para elegir cuando ocurre un miss. Hay dos estrategias primarias empleadas para seleccionar que bloque reemplazar: Random- Los bloques candidatos son seleccionados aleatoriamente. Usado menos recientemente(LRU): para reducir la posibilidad de desechar información que se necesitará en un futuro muy próximo, los accesos a bloque son registrados. El bloque que se reemplaza es el que no se ha usado por más tiempo. Esto hace uso del concepto de localidad temporal. Una virtud del random es que es simple de construir en HW. Cuando el número de bloques que se deben mantener aumenta, LRU se hace más costoso y es frecuentemente una aproximación. La política de reemplazo juega un mayor papel en caches pequeñas que en caches grandes donde hay más elecciones sobre cuál reemplazar. Otra política, FIFO, simplemente descarta el bloque que fue usado N accesos antes, independientemente de cuando se referencia.{COMPLETAR} Q4: Que sucede en una escritura? Para una escritura, el procesador especifica el tamaño de la escritura, usualmente entre 1 y 8 bytes; solo esa porción de un bloque puede ser cambiada. En general esto significa una secuencia de operaciones lectura-modificaciónescritura sobre el bloque: lectura del bloque original, modificar una porción, y escribir el nuevo valor del bloque. Además, la modificación de un bloque no puede comenzar hasta que el tag es chequeado para verificar si este es un hit. Como el chequeo de tag no pueden hacerse en paralelo, entonces, escribir normalmente toma más que la lectura(en operación de lectura el bloque puede ser leido al mismo tiempo que el tag es laido y comparado). Hay dos opciones básicas cuando escribimos cache: Write through (o store through). La información se escribe en el bloque en cache y en el bloque en memoria de lower-level. Write back. La información solo se escribe en el bloque en cache. El bloque cache modificado se escribe en memoria principal solo cuando este es reemplazado. Bloques cache write-back son llamados clean o dirty, dependiendo de si la información en la cache difiere de la que está en memoria de lower-level. Para reducir la frecuencia de reemplazos de bloques writings back, se usa una característica llamada dirty bit. Este bit de estado indica si el bloque fue modificado o no mientras estaba en cache. Si no lo fue, el bloque no es escrito, porque el lower-level tiene la misma información que en la cache.
63
Ambos métodos tienen sus ventajas. Con write back, las escrituras ocurren a la velocidad de la memoria cache, y múltiples escrituras dentro de un bloque requieren solo una escritura en la memoria de lower-level. Como cada escritura no va a memoria, write back usa menos ancho de banda de memoria, haciendo que el write back sea atractivo para multiprocesadores. Con write through misses de lectura no implica escribir en lower-level; además es más fácil de implementar que el write back. Write through tambien tiene la ventaja de que en memoria principal se tienen las copias más actuales de los datos. Esto es importante en multiprocesadores y para I/O. Cuando el CPU debe esperar para completar escrituras durante write throughs, se dice que el CPU tiene un write stall. Una optimización común para reducir los write stalls es un buffer de escritura, el cual permite que el procesador continue mientras que la memoria se está actualizando. Hay dos opciones cuando hay un miss de escritura: Write allocate. El bloque es cargado, seguido por las acciones de write-hit anteriores. Esto es similar a un miss de lectura. No write allocate. El bloque es modificado en el lower-level y no se carga en cache.
Los tres orígenes de los miss de cache son: 1. Compulsivo: el primer acceso a un bloque no está en cache, por lo tanto el bloque deberá ser llevado a cache. 2. Capacidad: si la cache no puede contener todos los bloques necesarios durante la ejecución de un programa, ocurrirán misses de capacidad debido a los bloques que son descartados y luego recuperados. 3. Conflictivos: si la estrategia de ubicación de bloque es set-asociativo o mapeo directo, ocurrirán misses de conflicto porque un bloque puede ser descartado y luego recuperado si demasiados bloques mapean a ese conjunto. Tambien son llamados misses de colisión. Habiendo identificado los tres causantes de misses de cache, que puede hacer un diseñador de computadoras acerca de ellos? Conceptualmente, los conflictivos son los más simples: organización full asociativo evita todos los misses de conflictivos. Asociatividad es costosa en HW, sin embargo, podría bajar el tiempo de acceso. Hay poco que hacer respecto a la capacidad excepto comprar más chips de memoria. Si la memoria de upper-level es más pequeña que la que se necesita para un programa, y un porcentaje significativo del tiempo se gasta moviendo datos entre los dos niveles de la jerarquía, la jerarquía de memoria se denomina thrash. Como se requieren muchos reemplazos, thrashing significa que la máquina corre cerca de la velocidad de la memoria de lower-level, o quizás se hace más lenta debido al overhead producido por el miss. Haciendo los bloques más grandes se reduce el número de misses compulsivos, pero se incrementan los misses de conflicto. Un miss podría moverse de una categoría a otra si cambian algunos parámetros. Por ejemplo, incrementar el tamaño de cache reduce los misses de conflicto y los de capacidad. Elección para el tamaño de bloques en cache: bloques grandes reducen los misses compulsivos, como sugiere el principio de localidad espacial. Al mismo tiempo, bloques grandes tambien reducen el número de bloques en la cache, incrementando los misses de conflicto. Caches de solo-instrucción o solo-dato Vs. Caches Unificadas: a diferencia de otros niveles de la jerarquía de memoria, las caches son a veces divididas en caches de solo-instrucción y caches de solo-datos. Las caches que pueden contener ambos, datos e instrucciones se llaman caches unificadas. La CPU conoce si se trata de una dirección de instrucción o dirección de datos, por lo tanto puede haber ports separados para ambos, de ese modo doblando el ancho de banda entre cache y CPU. Caches separadas tambien ofrecen la oportunidad de optimizar cada cache en forma separada: diferentes capacidades, tamaños de bloque, y asociatividades pueden llevar a una mejor perfomance. Esta división afecta el costo y perfomance más allá de lo que es indicado por los cambios en las miss rates. Limitaremos nuestra discusión a mostrar como los miss rates para instrucciones difieren de los miss rates para datos. Separar instrucciones y datos remueve los misses debido a los conflictos entre bloques de instrucciones y bloques de datos, pero la división tambien fija el espacio de cache dedicado a cada tipo.
64
Memoria Principal
Memorias de acceso aleatorio(RAM): las RAM pueden ser descriptas como que consisten de un número de Iunidades idénticas(en general palabras), cada una accesible a través de una dirección cableada, y cada una con la misma dirección de acceso independiente de la locación de las I-unidades y de los accesos previos. Escribir o leer en una RAM requiere: Un medio de almacenamiento organizado en tal forma que las I-unit(palabra) pueden ser leidas como entidades. Un registro de dirección (MAR-Memory Acces Register) para mantener la dirección de la palabra que está siendo accedida. Un registro de datos (MDR-Memory Data Register) o buffer para mantener la información que está siendo leida o escrita. Sensores para la lectura. Transducer (drivers) para escribir.
MDR
Lectura
S E N S O R E S
Medio de Almacenamiento
T R A N S D U C E R
Escritura
Mecanismo de Dirección
MAR
Representación esquemática de una RAM. El medio de almacenamiento es un arreglo de bits, con los bits subsecuentes agrupados en palabras. Ya que la unidad atómica es el bit, el medio de almacenamiento físico debe cumplir con las siguientes propiedades: Sensar uno de dos estados estables que serán 0 y 1 respectivamente. Este sensado se usará para la lectura. Cambiar desde un estado a otro siempre que se necesite para propositos de escritura. Este sensado y switching será realizado a través de la aplicación de una fuente de energía externa. Sin tener en cuenta el medio de almacenamiento, podemos ver que la lectura implica la selección de una palabra y el sensado de cada bit de la palabra, y que la escritura implica la selección de la palabra y una discriminación posterior sobre los bits a ser cambiados de un estado a otro. Así cualquier celda de almacenamiento será capaz de recibir entradas desde un selector de palabras, un selector de bits y un sensor. Sin embargo, estos dos pueden ser compartidos, ya que son usados en las operaciones mutuamente excluyentes de lectura y escritura. Mecanismo de direccionamiento: la dirección de la palabra a ser leida o escrita se encuentra en el MAR. Asumiendo que la RAM tiene una capacidad de S palabras de b bits(S=2nb), el MAR tiene n bits de ancho. Una 65
decodificación directa requerirá del orden de S compuertas para obtener una salida de las S(envío n lineas a cada punto). Esta técnica se llama selección lineal. Otra alternativa es decodificar enviando a cada lugar una línea. Dificultades: 1)carga excesiva sobre las lineas. La tarea de decodificación en cada punto podrá ser un HW excesivo. 2)problemas de layout(interconexión de compuertas). Visto esto una mejor alternativa sería direccionamiento multidimensional. Direccionamiento Multidimensional: ¿???????????????????????????
Memorias de Núcleo: la celda de almacenamiento básica es un núcleo /?????????????? Dependiendo del número de cables que enhebren núcleos individuales tenemos diferentes organizaciones de memoria. Organización 3-D: desde el punto de vista geométrico, las memorias de núcleo 3-D de W = 2nb bits-words consiste de b planos de w núcleos. Esto es, los bits en la posición i de las w palabras estan en el mismo plano. Entonces habrá un cable de sensado y uno de habilitación por plano conectado al bit relevante en el MDR. Cada núcleo será enhebrado por dos cables, los cables x e y que conducirán parte de la cte necesaria para cambiar o sensar los estados. El cable x corresponderá a la decodificación de la unidad inferior de la dirección(en el MDR) y el cable y a la mitad superior. Los procedimientos de lectura y escritura muestran como podemos mezclar los cables de inhibición y sensado, ya que son simplemente usados en tiempos diferentes y podrían ser compartidos. Para lectura: 1. Enviar –i/2 sobre el cable x y el cable y correspondiendo a la decodificación de las mitades respectivas de la dirección, amplificar las señales de sensado y entrarlas al MDR. 2. Regenerar enviando i/2 sobre los mismos cables x e y y –i/2 sobre el cable de inhibición para aquellos bits que son 0 en el MDR. Para escritura: 1. Clarear la palabra entrando –i/2 sobre el cable x y el cable y, pero sin amplificar las señales de sensado, es decir no alterando el MDR. 2. Idem paso 2) de lectura. Esta organizacin requiere: W.b núcleos para almacenar información. Un decoder x con n/2 entradas y 2n/2 = w salidas. Un decoder y con n/2 entradas y 2n/2 salidas. 2*2n/2 = 2 w drivers o conductores para los cables x e y. b cables de inhibición o sensado. Organización 2-D: conceptualmente el esquema 2-D parece más simple que el 3-D porque hace una selección lineal de palabras. Geometricamente la memoria 2-D tiene b lineas de ancho w. Los núcleos tienen que ser enhebrados unicamente por dos cables, uno para seleccionar la palabra y uno por bit. La última linea de bit puede ser usada para sensado y para cambio de estado. Para lectura: 1. Enviar –i sobre el cable selector de palabras. Amplificar las señales sensadas sobre las lineas de bit y entrar aquellas señales a los bits asociados an el MDR. 2. Regenerar entrando i sobre la línea de palabra y –i/2 sobre las lineas de bits de aquellos bits seteados a 0 en el MDR. Para escritura: 1. Idem lectura pero sin actualizar el MDR 2. Idem lectura. Esta organización requiere: W.b núcleos para almacenar información. 66
Un decodificador con n entradas y 2n = w salidas. W drivers. B lineas de inhibición y sensado. A primera vista, parece que pagamos caro la aparente simplicidad de este esquema, w drivers en vez de 2 w drivers y el mismo radio de complejidad para la decodificación. Pero se tiene la ventaja de necesitar un cable menos por núcleo. Como la velocidad de cambio y sensado está relacionado con el tamaño del núcleo, se pueden obtener memorias más rápidas con una organización 2-D.
RAM Eletrónica: la celda básica en una RAM electrónica estática será un FF. Conectados al FF hay dos lineas de sensado y bit, una para cada estado estable(0 y 1), y una linea de palabra para todos los bits de la palabra dada. Existe una diferencia importante entre los procesos de lectura y escritura para memorias de núcleo y RAM electrónicas; en el último caso el sensado es destructivo. Consecuentemente, las lineas de bit y las lineas de sensado pueden ser compartidas y las lineas de inhibición son innecesarias. Más aún, en el tiempo de ciclo, que es la suma de regeneración y acceso, la segunda componente será más pequeña que la primera en lugar de ser aproximadamente el mismo valor como en memoria de núcleo.
La RAM puede ser: De núcleo(hasta mediados de los 70’s). Semiconductora Bipolar-Memorias estáticas(FF) Estática(FF). Memoria cache(estática). 6 transistores por celda. MOS Dinámica(condensadores). Memoria principal(dinámica). Una celda dinámica solo un transistor.
La memoria principal satisface las demandas de caches y vector units, y sirve como la interface de I/O, es el destino de las entradas y el origen de las salidas. A diferencia de las caches, las medidas de perfomance de la memoria principal emfatiza latencia y ancho de banda. Generalmente la latencia de memoria principal(la cual afecta la cache miss penalty) es el tema primario de la cache, mientras que el ancho de banda de la memoria principal es el tema de mayor importancia de I/O y vector units. Como en los bloques de cache, el tamaño de un bloque va desde 4-8 bytes hasta 64-256 bytes. La latencia de memoria depende de dos medidas- tiempo de acceso y tiempo de ciclo. Tiempo de acceso es el tiempo entre que se requiere una lectura y la palabra deseada arriba., mientras que tiempo de ciclo es el tiempo mínimo entre requerimientos a memoria. DRAM(Dynamic RAM): si cada fila no es accedida dentro de un cierto período de tiempo(2 miliseg) la información puede perderse. Este requerimiento significa que el sistema de memoria está ocasionalmente no disponible porque está enviando una señal de refresco a cada chip. El costo de un refresco es un acceso a memoria para cada fila de la DRAM. En contraste a las DRAM están las SRAM(static RAM). La naturaleza dinámica de las DRAM requieren que el dato se escriba nuevamente luego de una lectura, por lo tanto hay una diferencia entre el tiempo de acceso y el tiempo de ciclo. Las SRAM usan más circuitos por bit para prevenir que la información se pierda luego de una lectura. A diferencia de las DRAM, no existe diferencia entre el tiempo de acceso y el tiempo de ciclo; además no hay necesidad de refresco. En los diseños de DRAM se pone emfasis en la capacidad, mientras que en la SRAM capacidad y velocidad.
Interleaving de Memoria: en ambientes de procesamiento paralelo, la meoria principal es un recurso primario que es normalmente accedido por todos los procesadores o las distintas unidades de un procesador pipeline. Para evitar la degradación de la perfomance por interferencia de memoria causada por dos o más procesadores simultaneamente accediendo a la misma unidad de memoria, es conveniente no tener una sola unidad de memoria 67
para todo el sistema. Por lo tanto la memoria principal es particionada en varios módulos de memoria independientes y el direccionamiento distribuido a través de esos módulos. Esta organización se conoce como interleaving y resuelve en parte los problemas vistos, permitiendo accesos concurrentes a más de un módulo. Si hay M módulos se llama M-way interleaving. Hay dos métodos básicos de distribuir la dirección entre los módulos. Asumiendo un total de M = 2n palabra en memoria principal. Entonces la dirección física para una palabra en memoria consiste de n bits an-1,an-2.....a1,a0. Un método es interleaving de alto orden, distribuye las direcciones en M = 2m modulos de forma tal que cada módulo i, 0 i M-1, contiene direcciones consecutivas de memoria i2n-m hasta (i+1)2n-m – 1. Los m bits superiores son usados para seleccionar el módulo, mientras que los restantes n-m seleccionan dentro del módulo. El segundo método, interleaving de bajo orden, distribuye la direcciones de forma tal que direcicones consecutivas sean localizadas en módulos consecutivos. Los m bits menos significativos menos signnificativos seleccionan el módulo y los restantes n-m seleccionan el address dentro del módulo. Luego una dirección A estará localizada en el módulo A mod m. El primer método permite fácil expansión de memoria agregando módulos hasta un máximo de M-1. Sin embargo la ubicación de direcciones de memoria contiguas dentro de un módulo causaría conflictos en el caso de procesadores pipeline y vector proccesors. La secuencialidad de instrucciones en un programa y la secuencialidad de datos en vector processors causan que instrucciones o datos consecutivos esten en el mismo módulo. Como el tiempo de ciclo de memoria es mayor que el tiempo de ciclo de pipeline, un requerimiento a memoria previo no completaría su acceso antes del arribo del siguiente requerimiento, lo que resulta en un retardo. Si los elementos de un vector residen en el mismo módulo, el paralelismo en la computación será insignificante porque los elementos no pueden buscarse simultaneamente por todos los procesadores. El interleaving de alto orden puede usarse sin conflictos en multiprocesadores si los módulos son particionados de acuerdo a procesos disjuntos. En la práctica, sin embargo, los procesos interactúan y comparten instrucciones y datos. Por lo tanto, se usa frecuentemente el interleaving de bajo orden para reducir interferencia de memoria. Una ventaja del interleaving de alto orden es que proporciona mayor confiabilidad. Dado que un módulo que falla afecta solo un área. El módulo que falla podría ser localizado y aislado del sistema a través del memory manager. En el otro esquema, un fallo en un solo módulo sería catastrófico para todo el sistema. No obstante se prefiere este último si la interferencia de memoria es la única base para la elección. Otra posible técnica(hibrido) es particionar el campo de la dirección del módulo en dos secciones Sm-r y Sr, tal que Sr es la parte menos significativa y Sm-r son los m-r bits de alto orden de la dirección. La dirección del módulo será la concatenación de la sección Sm-r y Sr. En este esquema, las direcciones son interleaved entre grupos de 2r módulos de memoria. Esto tiende a reducir las interferencias de memoria para un segmento de dato compartido. El sistema de memoria se puede expandir en bloques de 2r módulos, sin embargo, la falla de un solo módulo deshabilita un bloque completo de los 2r modulos. Este esquema es interesante para sistemas con un gran número de módulos de memoria si se opta por un tamaño de r muy pequeño. Nota: en camino a incrementar el ancho de banda efectivo entre PC y Mp tenemos distintos metodos: 1. Decrementar el tiempo de acceso usando memorias más rápidas 2. Usar una palabra más grande. En un acceso a memoria traigo palabras con mayor información. Gano tambien velocidad. 3. Insertar una memoria cache entre PC y Mp. 4. Acceder a más de una palabra durante cada ciclo de memoria(interleaving de memoria).
Memorias Asociativas: estas memorias tienen la capacidad de direccionar items por contenido en vez de por locación. Así, en vez de responder cuestiones como ¿cuál es el contenido de la locación LOC?, una memoria asociativa responde a cuestiones de la forma ¿hay alguna locación que contiene al item XYZ? Para hacer esto en forma eficiente todas las locaciones deben ser examinadas simultaneamente. Consecuentemente cada bit en la memoria asociativa requiere lógica en adición a los dos estados estables de memorias direccionables. El costo es por lo tanto mucho mayor. En general, una memoria asociativa es aquella en la cual cualquier item almacenado puede ser accedido directamente mediante el uso de los contenidos del item en cuestión, generalmente algún subcampo, como una dirección. Este subcampo es llamado key. Los items almacenados en una memoria asociativa pueden tener el siguiente formato: KEY, DATA 68
Donde KEY(page name por ejemplo) es una dirección y DATA (page frame, presence bit)la información a ser accedida. Una memoria asociativa o memoria direccionable por contenidos(CAM) tiene la siguiente estructura: Entrada
Registro de entrada
Registro máscara Key
Arreglo de Almacenamiento
Circuito Selector
Registro de salida
Salida Cada unidad de información almacenada es una palabra de longitud fija. Cualquier subcampo de la palabra puede seleccionarse como la clave(key). La clave deseada se especifica mediante el registro máscara. La clave se compara en simultaneo con todas las palabras almacenadas; aquellas que hacen match emiten una señal la cual entra a un circuito selector. El circuito selector habilita el campo de dato a ser accedido. Si algunas entradas tienen la misma clave, entonces el circuito selector determina que campo de dato se va a leer. Como todas las palabras en memoria son requeridas para comparar sus claves con la clave de entrada simultaneamente, cada una deberá tener su propio match circuit. Los circuitos de match y selección hacen que las memorias asociativas sean más complejas y costosas que la memorias convencionales. El advenimiento de técnicas LSI han hecho que las memorias asociativas sean más económicas. Sin embargo las consideraciones de costo las limitaran a aplicaciones donde una cantidad relativamente pequeña de información deba ser accedida muy rapidamente, por ejemplo, mapeo de direcciones de memoria. Tambien es usada por procesadores asociativos o en pequeñas caches. Podemos tener dos tipos de memorias asociativas: Bit paralelo: todas las palabras se comparan en paralelo con el contenido del registro de entrada, o en parte de este, según lo indique el registro máscara. Obviamente esto implica una gran cantidad de circuitería por bit. Bit serie: esta organización es más lenta pero tiene un HW menos costoso. Aquí se analiza de un bit por vez de la palabra. Habrá que hacer tantas comparaciones como bits tenga la palabra. En realidad es una comparación simultánea, pero de a un bit por palabra. En este caso necesitaríamos otro registro. Cuando no hace match, no tiene sentido seguir con la búsqueda.
69
Memoria Virtual La meoria virtual es una técnica que permite la ejecución de procesos que podrían no estar completamente en memoria. La principal ventaja de este esquema es que los programas pueden ser más largos que la memoria física. Esta técnica libera al programador de tratar con las limitaciones de memoria. Memoria virtual no es fácil de implementar, además, podría decrementar sustancialmente la perfomance si se usa descuidadamente. La capacidad de ejecutar un programa que está parcialmente en memoria tiene muchos beneficios: La longitud de un programa no está restringida por la cantidad de memoria física disponible. Los usuarios podrían ser capaces de escribir programas para un extremadamente grande espacio de direccionado virtual, simplificando la tarea de programación. Como cada programa de usuario podría tomar menos memoria física, más programas podrían ser corridos al mismo tiempo, con el correspondiente incremento en utilización de CPU y throughput, pero sin incrementar el tiempo de respuesta No se necesitaría tanta I/O para cargar o hacer swap de cada programa de usuario a memoria, por lo tanto cada programa podría correr más rápido. La memoria virtual generalmente se implementa mediante demand paging. Los procesos residen en memoria secundaria(usualmnete disco). Cuando se quiere ejecutar un proceso, lo ponemos en memoria. En lugar de hacer swapping del proceso entero, el pager(manipulador de páginas) lleva solo aquellas paginas necesarias a memoria, decrementando el swap time y la cantidad de memoria física necesaria. Se utiliza un soporte de HW para distinguir aquellas páginas que estan en memoria de las que están en disco. Se utiliza el esquema de bit válido-inválido para este propósito. Si un proceso trata de usar una página que no está en memoria se produce un page fault, entonces se salva el estado del proceso interrumpido, se encuentran frames libres,se hace una operación de disco para poner la página deseada en el frame alocado, se modifica la page table para indicar que la página está ahora en memoria y luego se reanuda la ejecución del proceso en el mismo lugar y estado donde se había interrumpido, excepto que ahora la página deseada está en memoria y es accesible. De esta manera es posible ejecutar procesos, aunque porciones de este todavía no esten en memoria. Este proceso es transparente al usuario. El HW para soportar demand paging es: una page table y memoria secundaria(disco). Algoritmos de reemplazo de página: si no hay frames libres, buscamos uno que no sea actualmente usado y lo liberamos. Podemos liberar un frame escribiendo su contenido en el swap-space(disco), y cambiando la page table para indicar que la página ya no está en memoria. El frame liberado puede usarse ahora para poner la página que causó el page fault. Algoritmo FIFO: este algoritmo asocia con cada página el tiempo en que esa página fue llevada a memoria. Cuando se deba reemplazar una página se elige la más vieja. Podemos crear una cola FIFO para almacenar todas las páginas en memoria. Se reemplaza la página que está en la cabeza de la cola. Cuando se inserta una página en memoria, la insertamos al final de la cola. La perfomance de este algoritmo no siempre es buena. La página reemplazada podría ser un módulo de inicialización que fue usado un largo tiempo atrás y no es muy necesaria. Por el otro lado, podría contener una variable que es muy usada y que fue inicializada tempranamente y está en constante uso. Despues de hacer page out de una página activa para traer una nueva, un fault ocurre casi inmediatamente para recuperar la página activa. Esto es, una mala elección en reemplazo incrementa el promedio de page faults y baja el tiempo de ejecución del proceso, pero no produce una ejecución incorrecta. Este algoritmo tiene la anomalia de Belady la cual dice: para algunos algoritmos de reemplazo de página, la razón de page fault se incrementa cuando el número de frames alocados se incrementa Algoritmo Optimal: el uso de este algoritmo de reemplazo de pagina garantiza el promedio más bajo posible de page faults para un número fijo de frames(nunca sufre la anomalía de Belady). Tambien es llamado OPT o MIN. Consiste en simplemente reemplazar la página que no será usada durante el período de tiempo más largo. Desafortunadamente este algoritmo es dificil de implementar, porque requiere conocimiento futuro del string de referencias. Como resultado este algoritmo es usado principalmente para estudios de comparación.
70
Algoritmo LRU(usado menos recientemente): este algoritmo asocia con cada página el tiempo de la última vez que se uso. Cuando se deba reemplazar una página, el LRU selecciona la página que no se haya usado por el período más largo de tiempo. Este algoritmo es bastante bueno, el problema que surge es como implementarlo. O sea, determinar un orden para los frames definido por el tiempo de uso último. Hay dos implementaciones factibles: Contadores: es el caso más simple, asociamos con cada entrada a la page table un campo de tiempo de uso, y agregamos a la CPU un reloj lógico o contador. El reloj se incrementa con cada referencia a memoria. Dondequiera que se hace una referencia a memoria, los contenidos del registro reloj se copian en el campo tiempo de uso en la page table para esa página. Se reemplaza la página con el valor de tiempo más pequeño. Este esquema requiere una búsqueda en la page table para encontrar la página LRU y una escritura a memoria (al campo tiempo de uso en la page table) por cada acceso a memoria. Stack: otro approuch de implementar el reemplazo LRU es mantener una pila de números de páginas. Dondequiera que una página es referenciada, es removida de la pila y puesta en el tope. De esta manera, el tope de la pila es siempre la página usada más recientemente y parte más baja es la página LRU. Como las entradas tienen que ser removidas desde el medio de la pila, la mejor implementación es una lista doblemente enlazada, con un puntero a la cabeza y a la cola. Cada actualización es un poco más costosa, pero no hay búsqueda para un reemplazo. Este approuch es apropiado para implementaciones por SW o microcódigo. Ni el Optimal ni el LRU sufren de la anomalía de Belady. Existe una clase de algoritmos de reemplazo de página, llamados algoritmos stack, que nunca exhiben la anomalía de Belady. Un algoritmo stack es un algoritmo por el cual puede mostrarse que el conjunto de páginas en memoria para n frames es siempre un subconjunto del conjunto de páginas que estarían en memoria con n+1 frames. Para LRU, el conjunto de páginas en memoria serían las n más recientemente referenciadas. Si se incrementa el número de frames, estas n páginas serán todavía las más recientemente referenciadas y todavía estarán en memoria. Ninguna implementación de LRU es posible sin la asistencia de HW más ayá de los registros TLB estandar. La actualización del campo reloj o pila se deben hacer para cada referencia a memoria, y hacerla por SW causa overhead para el manejador de memoria. Prepaginado: una característica de un sistema demand-paging es el gran número de page-faults que ocurren cuando comienza un proceso. El prepaginado es un intento de prevenir este alto nivel de page-fault inicial. La estrategia es llevar a memoria de una vez todas las páginas que se necesitarán. El prepaginado tiene sus desventajas en algunos casos. Por ejemplo, que ocurre si muchas de las páginas que se llevaron a memoria mediante prepaginado no son usadas. Por este motivo habría que analizar si el costo del prepaginado es menor que el costo de servir los correspondientes page-faults. Tamaño de Página: el tamaño de una página puede estar entre 512-16384 bytes. Para un dado espacio de memoria virtual, decrementar el tamaño de las páginas, incrementa el número de páginas, y por lo tanto el tamaño de la page table. Como cada proceso activo deberá tener su propia copia de la page table, es preferible tener un tamaño de página grande. Por otro lado, si queremos minimizar la fragmentación interna(generalmente la última página para un dado proceso no se llena totalmente) es preferible un tamaño de página pequeño. Otro problema es el tiempo requerido para leer o escribir una página. El tiempo de I/O está compuesto por tiempos de búsqueda, latencia, transferencia. El tiempo de transferencia es proporcional a la cantidad transferida(esto es, el tamaño de página) lo cual sería un argumento a favor del tamaño de página pequeño. Pero hay que tener en cuenta que la latencia y el seek time son factores que pesan mucho. Por ejemplo, puede tomar mucho menos tiempo leer una página de 1024 bytes, que dos páginas de 512 bytes cada una. Entonces, si se desea minimizar el tiempo de I/O es mejor un tamaño de página grande. Fragmentación Interna y Externa: cuando los procesos son cargados y luego liberados de memoria, el espacio de memoria libre resultante queda paricionado en pequeñas piezas. Hay fragmentación externa cuando existe espacio de memoria suficiente para satisfacer un requerimiento, pero este no es contiguo. Este problema de fragmentación pude ser severo. En el peor de los casos, podríamos tener un bloque de memoria libre cada dos procesos. Si toda esta memoria estuviera en un gran bloque libre, podríamos ser capaces de correr 71
más procesos. La selección de first-fit versus best-fit puede afectar la cantidad de fragmentación. Estos algoritmos examinan los espacios libres y determinan cual es el mejor para alocar. First-Fit: aloca el primer espacio que es lo suficientemente grande. Best-Fit: aloca el menor espacio que es lo suficientemente grande. Worst-Fit: aloca el espacio más grande. Otro factor es que extremo del bloque libre se aloca. La fragmentación externa puede ser mayor o menor dependiendo de la cantidad de memoria y el tamaño promedio de los procesos. Otro problema que surge con la alocación de particiones múltiples es: supongamos que tenemos una partición de 18464 bytes y un requerimiento de 18462 bytes. Si alocamos exactamente el bloque requerido, nos quedan libres dos bytes. El overhead para mantener este hole puede ser mayor que el hole en si mismo. El approuch a tener en cuenta es alocar holes muy pequeños como parte de un requerimiento grande. De ese modo la memoria alocada puede ser levemente superior a la memoria requerida. La diferencia entre estos dos números es la fragmentación interna- memoria que es interna a una partición, pero no está siendo usada. Una solución al problema de fragmentación externa es compactación. El objetivo es poner toda la memoria libre en un gran bloque. La compactación no se puede hacer si la relocación es estática. Solo puede hacerse si la relocación es dinámica, y es hecha en tiempo de ejecución. Si las direcciones son realocadas dinamicamente, la relocación requiere solo mover el programa y los datos, y luego cambiar el registro base para reflejar la nueva dirección base. Swapping tambien puede combinarse con compactación. Un proceso puede ser rolled out de memoria principal a backing store y más tarde rolled in. Cuando elo proceso es rolled out, se libera memoria que puede ser reusada por otro proceso. Cuando el proceso es rolled in pueden surgir algunos problemas. Si se usa relocación estática, el proceso deberá ser puesto en las mismas locaciones de memoria que ocupaba previamente. Esta restricción puede requerir que otros procesos en memoria deban ser rolled out para liberar esa memoria. Si se usa relocación dinámica los procesos pueden ser rolled en distintas locaciones. Paginado: la memoria física es dividida en bloques de tamaño fijo llamados frames. La memoria lógica tambien se divide en bloques del mismo tamaño llamados páginas. Cuando un proceso va a ejecutarse, sus páginas se cargan en cualquier frame de memoria disponible desde el backing store. El backing store está dividido en bloques de tamaño fijo que son del mismo tamaño que los frames de memoria. Cada dirección generada por el CPU se divide en dos partes: un número de página(p) y un offset(d). El número de página es usado como un indice en la page table. La page table contiene la dirección base de cada página en memoria física. La dirección base se combina con el page offset para definir la dirección física de memoria. El tamaño de página(como el tamaño de frame) está definido por el HW. El tamaño va desde 512-8192 bytes(potencias de dos- hace más facil la translación de una dirección lógica en un número de página y un offset) por página, dependiendo de la arquitectura. Si eltamaño del espacio de direcciones lógicas es 2 m, y el tamaño de página es 2n, entonces los m-n bits de más alto orden de la dirección lógica designan el número de página, y los n bits de menor orden designan el page offset Con este esquema no se produce fragmentación externa: cualquier frame libre puede ser alocado por un proceso que lo necesite. Sin embargo tenemos alguna clase de fragmentación interna. Por ejemplo puede ser que el último frame alocado por un proceso no se ocupe en su totalidad. En el caso de tener paginas de 2048 bytes, y un proceso de 72766 bytes se necesitarían 35 páginas más 1086 bytes. Luego se alocarán 36 páginas produciendo una fragmentación interna de 2048-1086=962 bytes. En el peor caso se podrían necesitar n páginas más un byte. Entonces se tendría que alocar n+1 frames, resultando una fragmentación interna de casi un frame. Esta consideración sugiere que el tamaño de las páginas sean pequeños. Sin embargo, existe un bit de overhead involucrado en cada entrada a la page table, y este overhead se reduce cuando el tamaño de la página se incremeta. Tambien, las I/O de disco son más eficientes cuando el número de datos a transferirse es mayor. Cuando un proceso arriba para ejecutarse, su tamaño expresado en páginas, es examinado. Cada página de usuario necesita un frame. La primera página del proceso es cargada dentro de uno de los frames alocados, y el número de frame se pone en la pge table para ese proceso. La página siguiente se carga en otro frame, y su número de frame se pone en la page table y así siguiendo.
72
El S.O. mantiene una copia de la page table para cada proceso, como lo hace con el instruction counter y el contenido de registros. Esta copia se usa para transladar direcciones lógicas en direcciones físicas. Es claro que el paginado aumenta el tiempo de cambio de contexto. En el caso más simple la page table se implementa como un conjunto de registros dedicados. Debido a su rapidez la translación de direcciones de paginas es eficiente.
El uso de registros es factible si la page table es razonablemente pequeña. Actualmente la mayoria de las computadoras, permiten que la page table sea muy grande; por lo tanto el uso de registros rápidos no es factible. Entonces, se puede mantener la page table en memoria, y un page-table base register(PTBR) apunta a la page table. Cambiar de page table solo requiere cambiar este único registro, reduciendo sustancialmente el tiempo de cambio de contexto. El problema con este approuch es el tiempo requerido para acceder a la locación de memoria de usuario. Se requieren dos accesos a memoria(uno para la entrada en la page table, uno para el byte). La solución a este problema es usar una cache, llamada translation look ahead buffer(TLB) o registros asociativos. Los registros asociativos contienen solo una parte de todas las entradas de la page table. Cuando el CPU genera una dirección lógica, su número de página es presentado a un conjunto de registros asociativos que contienen números de página y sus correspondientes número de frame. Si se encuentra el número de página, el número de frame está disponible para usarse para acceder a memoria. Si el número de página no se encuentra en los registros asociativos, se debe hacer una referencia a memoria hacia la page table. Cuando se obtiene el número de frame, podemos usarlo para acceder a memoria. Además, se agrega el número de página y el número de frame a los registros asociativos; de ese modo se encontrarán rapidamente en la próxima referencia. Si la TLB está completa, el SO deberá seleccionar una para su reemplazo. Desafortunadamente, cada vez que se seleeciona una nueva page table(por ejemplo, cada cambio de contexto), la TLB es flushed para asegurar que el próximo proceso a ejecutarse no use información de translación equivocada. La protección de memoria en un ambiente paginado se logra mediante bits de protección que se asocian con cada frame. Normalmente, estos bits se mantiene en la page table. Un bit puede definir una página como de lectura y escritura o solo lectura. Cada referencia a memoria recorre la page table para encontrar el número de frame correcto. Al mismo tiempo que se computa la dirección física, se pueden chequear los bits de protección para verificar que no se hagan escrituras en una página de solo lectura. Segmentación: el espacio direccionado es una colección de segmentos. Cada segmento tiene su nombre y longitud. El usuario especifica cada dirección mediante dos cantidades: el nombre del segmento y el offset dentro del segmento. Por simplicidad de implementación, los segmentos se numeran y son referenciados mediante un número de segmento. Entonces, una dirección lógica consiste de una tupla: <número de segmento,offset>. Normalmente, el compilador construye automaticamente los segmentos. Por ejemplo un segmento para las variables globales, la pila de llamadas a procedimiento, la porción de código de cada procedimiento o función, las variables locales de cada procedimiento o función,etc. El mapeo entre direcciones lógicas y físicas se efectúa mediante la segmet table. Cada entrada de la segment table tiene una base de segmento y un límite de segmento. La base de segmento contiene la dirección física de comienzo donde el segmento reside en memoria, mientras que el límite de segmento especifica la longitud del segmento. Una dirección lógica consiste de dos partes: un número de segmento ,s, y un offset dentro de ese segmento,d. El número de segmento es usado como un índice en la segment table. El offset d de la dirección lógica deberá estar entre 0 y el límite del segmento. Si no es así se produce un trap del SO. Si el offset es legal, este es sumado a la base de segmento para producir la dirección en memoria física del byte deseado. Al igual que la page table, la segment table puede ser puesta en registros rápidos o en memoria. Una segment table que se mantiene en registros puede referenciarse rapidamente; la adición de la base y la comparación puede hacerse simultaneamente para ahorrar tiempo. Si un programa consiste de un gran número de segmentos, no es factible el uso de registros, es mejor usar memoria. Un segment-table base register (STBR) apunta a la segment table. Además, como la cantidad de segmentos usados por un programa puede variar ampliamente, se usa un segmenttable lenght register(STLR). Para una dirección lógica, primero se chequea si el número de segmento s es legal(s<STLR). Luego, sumamos el número de segmento al STBR, resultando en la dirección en memoria de la entrada en la segment table. Esta entrada se lee desde memoria y se procede como sigue: se chequea el offset con la longitud del segmento y se computa la dirección física del byte deseado como la suma de la base del segmento más el offset. 73
Al igual que con paginado hay dos accesos a memoria por dirección lógica. La solución adoptada es la misma que para paginado: un conjunto de registros asociativos. Una ventaja de la segmentación es la asociación de protección con los segmentos. El HW de mapeo de memoria chequerá los bits de protección asociados con cada entrada en la segment-table para prevenir accesos ilegales a memoria como intentar escribir en un segmento de solo lectura, o usar un segmento de solo ejecución como dato. La segementación puede causar fragmentación externa, cuando todos los bloques libres de memoria son demasiado pequeños para acomodar un segmento. En este caso simplemente el proceso deberá esperar hasta que haya más memoria disponible o puede usarse compactación para crear un gran hole. Se podría eliminar la fragmentación externa poniendo un byte por segmento. Sin embargo cada byte necesitaría un registro base para su relocación, doblando el uso de memoria. Generalmente, si el tamaño de segmento es pequeño, la fragmentación externa tambien será poca. Segmentación con Paginado: el paginado elimina la fragmentación externa y hace que el problema de alocación sea trivial: cualquier frame vacio puede ser usado por la página deseada. Para la translación, el número de página indexa en la page table para obtener el número de frame. Finalmente, el número de frame se combina con el offset de página para formar la dirección física. La diferencia entre esta solución y segmentación pura es que la entrada en la segment table no contiene la dirección base del segmento, sino la dirección base de la page table para ese segmento. Como con paginado, la última página de cada segmento generalmente no estará llena. Entonces, se tiene, en promedio, media página de fragmentación interna por segmento. Por lo tanto, eliminamos la fragmentación externa pero introducimos fragmentación interna y incrementamos el overhead tabla-espacio.
SUERTE !!!!!!!!
74