Debido a que los programadores quieren memorias rápidas y de gran tamaño de almacenamiento (y que actualmente no pueden conseguirse a un bajo coste) se crea la ilusión de una gran memoria rápida que actua segun diferentes jerarquías.
Cuanto mas rapida es una memoria menos capacidad de almacenamiento posee y viceversa. La memoria SRAM (Static RAM) tiene un tiempo de acceso menor que 3ns, la memoria DRAM (Dynamic RAM) tiene un tempo de acceso de 70ns, el disco duro tarda hasta 20 millones de ns.
Si la jerarquia de memoria funciona (o lo parece) a igual ritmo que la memoria del nivel mas alto es porque existe el principio de localidad. Los programas muchas veces pediran datos que o bien ya se han pedido antes o bien estan muy cerca de los que se han pedido.
El objetivo es detectar estas zonas de memoria (datos y código) con mas probabilidad de ser accedidas y llevarlas a la memoria más rapida (para que se ejecuten tan rapido como proporcional es su necesidad de uso).
Existen dos tipos de localidad:
Temporal: El dato que se necesita consultar será consultado mas tarde (o fue consultado anteriormente).
Espacial: El dato que he consultado está cerca de otros que consultaré en breve (o lo busco cerca del que ya consulté).
Asi pues el principio de localidad busca exprimir al máximo la localidad temporal y espacial de un programa, buscando coger los datos que mas se han solicitado y los mas cercanos a ellos.
CONCEPTOS DE LA JERARQUÍA DE NIVELES
Cuanto mas cercano sea un nivel al procesador, mas pequeño será en tamaño pero mas rápido será. Los datos solo se copian entre niveles adyacentes asi si hay que traer algo desde disco duro tardamos el tiempo de acceso de recorrer toda la cadena dos veces.
La unidad minima de informacion se conoce como bloque.
Se produce un acierto (hit) si el elemento que pide la CPU está en el nivel superior.
Se produce un fallo (miss) si el elemento que pide la CPU no está en el nivel superior. En tal caso se llama al nivel inferior para que lo recupere, si no lo tuviera se llamaría a su inferior…
La tasa de aciertos (hit-rate) es la fraccion de accesos a memoria encontrados en el nivel superior. La tasa de fallos será por tanto todos los accesos que fallaron al solicitar un dato (1-hit.rate).
El tiempo de acierto es el tiempo necesario para acceder al nivel superior de la memoria, incluyendo el tiempo para determinar si el acceso es un fallo o un acierto.
La penalizacion por fallo es el tiempo necesario para reemplazar un bloque del nivel superior por otro bloque del nivel inferior.
VISION GENERAL DE LA CACHÉ
La caché fue el termino escogido para representar el nivel de la jerarquía de memoria entre la CPU y la memoria principal.
En general trabajaremos con 32 bits, no obstante seguiremos esta “creencia” salvo que se indique lo contrario en un ejercicio. Con 32 bits, nuestras direcciones de memoria son de 32 bits (logicamente) al mismo modo tenemos 2^32 direcciones que son 4GiB, donde en cada una de esas direcciones se graba 1 palabra (4bytes).
DIRECCIONES DE BYTES Y PALABRAS
Dir(palabra) = Dir(byte) DIV Tam(palabra)
Dir(palabra) = Dir(32) DIV Tam(4) -> Dir(Palabra) = 30 bits primeros.
Offset(palabra) = Dir(byte) MOD Tam(palabra)
Offset(palabra) = Dir(32) MOD Tam(4) = 2 bits ultimos.
La direccion de palabra se utiliza para localizar la palabra en la memoria, en cambio offset se utiliza para saber donde se localiza cada fragmento de la palabra. Es decir uno nos dice en que fila está y otro en que columna.
DIRECCIONES DE BYTES Y BLOQUES
Supongamos que tenemos un bloque de 8 bytes. Con 32 bits en MIPS.
Dir(bloque) = Dir(byte) DIV Tam(Bloque)
Dir(bloque) = Dir(32) DIV Tam(8) -> Dir(bloque) = 29 bits primeros
Offset(bloque) = Dir(byte) MOD Tam(Bloque)
Offset(bloque) = Dir(32) MOD Tam(8) = 3 ultimos bits.
La direccion de bloque se utiliza para localizar donde está el bloque de palabras, dentro de los cuales cada uno tendrá un dir-palabra y offset-palabra, en cambio el offset-bloque se utiliza para localizar donde está cada parte del bloque.
La caché generalmente tiene dos caracteristicas, conjuntos y vias. Cada conjunto es el numero de “cajones” mientras que cada vía son los huecos dentro de ese cajon. Asi pues existen tres tipos de cachés:
Directa: Muchos conjuntos con solo 1 via, solo puede meterse un dato por conjunto.
Asociativa: Varios conjuntos con al menos 2 vias, pueden meterse mas de un dato por conjunto.
Totalmente asociativa: Un solo conjunto con muchas vías, pueden meterse tantos datos como vias.
Esto tiene sus ventajas, en una caché de correspondencia directa la tasa de acierto es baja pero es muy rapida. Si es totalmente asociativa es lenta pero es bastante precisa. Generalmente en los procesadores actuales las cachés de primer nivel tienen una asociatividad baja, mientras que cuanto mas bajan suelen tener hasta 16 o 32.
Aunque ya sabemos donde se guardan los bloques y donde se guardan las palabras en la memoria no sabemos como se organizan los conjuntos. Los conjuntos es donde almacenamos un bloque o donde los buscamos.
Conjunto = Dir(bloque) MOD Num(conjuntos)
Si queremos buscar un bloque en caché miramos en el conjunto y todas sus vías (o en la unica si es directa) para almacenar un bloque en caché habrá que calcular su conjunto y buscar alguna via libre o remplazarla si no la hay.
Ejemplo de caché de correspondencia directa con 8 entradas y una memoria principal de 32 bloques.
Conjunto = Dir(32) MOD Num(8) -> Conjunto = 3 ultimos bits.
Conjunto = 00000000000000000000000000000-010 -> Conjunto = 010
Conjunto = 00000000000000011100000010001-010 -> Conjunto = 010
Para determinar la vía a la que corresponde un bloque utilizamos la etiqueta. Si el conjunto es la parte en comun de todas las direcciones de bloque, la etiqueta esl a unica parte que no tienen en comun (los otros 29 bits) en tal caso en nuestro ejemplo la etiqueta del segundo sería (00000000000000011100000010001).
Etiqueta = Dir(bloque) DIV Num(conjuntos)
Si alguna de las vias contiene un bloque valido hay un acierto, si no es un fallo. Para saber si se produce un fallo o no se añadirá un bit (V) de validez.
EJEMPLO DE ESTRUCTURA CON LA CACHÉ
Dirección | Dir(byte) | Conjunto(S) | Etiqueta(E)
0000100 | 00001 | 001 | 00
0100100 | 01001 | 001 | 01
1000100 | 10001 | 001 | 10
POLITICAS DE LA ESCRITURA
Generalmente para escribir en una caché hay dos politicas:
Write Through: Se sobreescribe sin importar los principios de localidad o temporalidad, es mucho mas tosco pero no requiere de una politica de sobreescritura (con un bit extra), se sustituye tanto la caché como la memoria principal.
Write Back: Las escrituras solo tienen lugar en la cache y solo cuando ese bloque tiene que ser desechado pues va a ser sustituido por otro se graba en memoria, asi solo hacemos 1 escritura en memoria. Esto requiere un bit de modificacion (M) que indica si ha de escribirse en la memoria tras sustituir el bloque en la caché.
POLITICA DE SOBRE-ESCRITURA
La caché ha fallado, asi que hay que traer desde memoria a la caché el bloque solicitado y colocarlo en su posicion.
Si fuera de correspondencia directa esa posición está ocupada por otro bloque y se sustiuye por el nuevo (a saco).
Si fuera asociativa, y el conjunto al que le corresponde está lleno se utiliza un algoritmo de sustitucion para decidir cual será machacado. Puede ser al azar o por LRU (least-recently-used), a través de un bit de uso (U) asi mismo esta es una pseudo-implementacion que es la mas comun.
Al final tenemos 3 bits extras (uso, modificacion y validez) que se añaden a la etiqueta y el dato en cuestion del conjunto.
EJEMPLOS CON CACHÉS: EJEMPLO 1.
Caché de correspondencia directa de 1024 entradas. Tamaño de bloque de 1 palabra, de 32 bits, escritura directa y 32 bits de direcciones.
Lo primero que tenemos que hacer es sacar los datos que nos da el propio enunciado:
Tamaño Caché = 1024 entradas * 32 bits = 32768 bits = 4096 bytes = 4KB.
Num.Conjuntos = 1024 (correspondencia directa).
Num vias = 1. 0 bits para vias.
Tenemos una caché que mide 1024 de largo con una sola via, tiene 1 bit de validez.
Nuestra direccion de Byte es de 32. Dado que los bloques son de 1 palabra, cada bloque tiene 4 bytes (32bits) asi que uso 2 como off-set (desplazamiento de byte), el resto (30bits) es la direccion de bloque.
Nuestra direccion de bloque es de 30. Dado que hay 1024 entradas necesito log2(1024) para redireccionarlo. 10 bits serán los que utilice para desplazamiento de bloque y el resto como etiqueta.
Conclusion, la etiqueta mide 20 bits, el off-set-bloque 10. La direccion de bloque 30 y los otros 2 bits conforman el off-set-byte. Al final nuestra caché tendra 1 bit de validez, 20 de etiqueta y los otros 32 bits de datos.
Off-set-Byte = 2 nº de byte.
Off-set-Bloque(Indice) = 10 bits
EJEMPLO CON CACHÉS: EJEMPLO 2
Caché de correspondencia directa de 64KiB, tamaño de bloque de 4 palabras de 32 bits. Direcciones de 32 bits y escritura directa.
Lo primero que tenemos que hacer es sacar los datos que nos da el propio enunciado. La unica diferencia con el enunciado anterior es que no sabemos los huecos y que las palabras tienen 4 zonas posibles en las que almacenarse en el mismo bloque.
Tamaño de Bloque = 4 palabras = 4 * 4bytes = 16 Bytes.
Numero de Entradas = 64*1024/16 = 4096 huecos.
Entonces nuestra caché mide 4096 de altura, tiene un bit de validez, una etiqueta (cuya longitud desconocemos). Y los datos que ocupan 4 bytes en cada uno de los 4 huecos.
Nuestra direccion de Byte es de 32, dado que tenemos 4 bloques y 4 bytes por palabras, necesitamos 4 bits (2 para el numero de bloque y 2 para los 4 bytes de cada palabra. Asi que nuestro off-set será de 4 y los otros 28 serán la direccion de bloque.
Nuestra direccion de bloque es de 28 bits, de los cuales utilizaremos log2(4096) para redireccionarlos, asi que 12 bits serán para el numero de la entrada y el resto (16) como etiqueta.
Off-set-Byte = 4 | 2 nº de palabra y 2 nº de byte.
Off-set-Bloque(Indice) = 12 bits
Tras mirar el indice (nº de entrada), si el bit de validez es 1 y coincide con la etiqueta (con un comparador entre lo que hay en la etiqueta y los n bits de la etiqueta del dato que entra) podemos saber si es un acierto o un fallo. Los 4 bits del desplazamiento 2 se utilizan para saber que palabra llamamos (de las cuatro que hay) y los ultimos dos para el byte dentro de esa palabra (de los cuatro que hay.
CACHES ASOCIATIVAS POR CONJUNTOS
Ya hemos visto como se organizan las direcciones de byte y de palabra dentro de las diferentes cachés, siempre dependientes del tamaño de cachés, el numero de vias y el numero de palabras.
EJERCICIO 1) Supongamos entonces que tenemos una tabla como la siguiente que corresponde a la evolución del contenido de una memoria caché de correspondencia directa de 4 posiciones:
RECORDATORIO:
Posición Caché: Dir.Bloque MOD 4(nº de posiciones).
Será acierto si consultamos el mismo dato que el almacenado en la posicion de la caché a la que corresponde.
Si se escribe en otra posicion se mantiene lo que hubiera escrito antes, sino se sobreescribe.
Y nos dan en el orden estricto una serie de bloques cuya direccion son:
0,8,0,6,8
0000,1000,0000,0110,1000
Pos(00) = 0; ponemos en memoria 0 M[0]
Fallo (en memoria está el dato 0)
Pos(00) = 8; ponemos en memoria 8; M[8]
Fallo (en memoria está el dato 0)
Pos(00) = 0; ponemos en memoria 0; M[0]
Pos(10) = 6; ponemos en memoria 6; M[6]
Fallo (en memoria estaba el dato 0)
Pos(00) = 8; ponemos en memoria 8; M[0]
Podemos ver que la caché de correspondencia directa tiene, en este caso un 0% de aciertos. Al final la tabla queda de la siguiente manera:
EJERCICIO 2) Supongamos la misma sucesión de datos en una caché asociativa de 2 conjuntos, con 2 bloques por conjunto (2 vias).
Para redireccionar 2 conjuntos solo necesitamos un bit, como podemos almacenar dos, si un dato va a un conjunto con un hueco libre, lo ocupa, si hay dos se sustituye uno ‘al azar’. El resto es la etiqueta.
Fallo (primer acceso: 1ª pos)
Conjunto(0) = 0; ponemos en memoria 0; M[0]
Fallo (primer acceso; 2ª pos)
Conjunto(0) = 8; ponemos en memoria 8; M[8]
Acierto (M[0] está en el conjunto 0).
Fallo (en memoria no está M[6] sustituyendo M[0])
Conjunto(0) = 6; pongo en memoria 6; M[6]
Fallo (en memoria no está M[0])
Conjunto(0) = 0; pongo en memoria 0; M[0]
Como podemos ver, la cache asociativa de dos conjuntos y dos bloques tiene una tasa de aciertos del 20%.
EJERCICIO 3) Supongamos la misma sucesión de bloques en una caché totalmente asociativa. Como no tiene conjuntos (solo 1) toda la direccion de byte es etiqueta.
Fallo (primer acceso, 1ª pos)
Conjunto unico = 0; pongo en memoria 0; M[0]
Fallo (primer acceso, 2ª pos)
Conjunto unico = 8; pongo en memoria 8; M[8]
Fallo (primer acceso, 3ª pos)
Conjunto único = 6; pongo en memoria 6; M[6]
Como se puede apreciar esta cache totalmente asociativa tiene un 40% de tasa de aciertos.
CONCLUSION: Aunque los fallos en las cachés directas son mas frecuentes, son mas baratas de construir pero las asociativas, cuanto mas asociativas sean menos fallos tienden a tener pero consultar un dato es mucho mas costoso.
Tamaño util caché = nºconjuntos x bloques.conjunto (asociatividad) x tamaño.bloque (bits).
Tamaño bloque = palabras.bloque x bits.palabra.
Tamaño total cache = nºconjuntos x bloques.conjunto x (nºbits.control + tamaño.etiqueta + tamaño.bloque).
Tamaño etiqueta = tamaño.direccion - nºbits.indice - nºbits.desplazamiento.palabra - nºbits.desplazamiento.byte.
EJEMPLO: Calcule el tamaño total de una caché de correspondencia directaque tiene las siguientes caracteristicas:
2 bits de desplazamiento de yte.
0 bits de desplazamiento de palabra (correspondencia directa, 1 bloque por conjunto).
Tamaño de direccion de memoria 32 bits
Nº total de cache = 2^10 x 1 x (1 + (32-10-2) + 32) = 54272 bits ) 6784 bytes.
Para calcular la velocidad a la que se ejecuta la caché realizamos las siguientes operaciones. Considerando Tcpu como el tiempo de funcionamiento normal del procesador.
Tejec = Tcpu + Tbloq_mem.
Tbloq_mem = Nºaccesos_mem x Tasa.Fallo x Penalizacion.Fallo
En general, no podemos decir que la tasa de fallos y la penalizacion sea la misma dado que hay operaciones que acceden a memoria o no. Sabiendo cuantas instrucciones acceden a datos en memoria (D/I).
Tbloq_mem = Nºaccesos_mem x (TFi x PFI + D/I x TFd x PFd).
Tejec = Nºinstr x CPIreal x Tciclo.
CPIreal = CPIideal + TFi x PFi + D/I x TFd x PFx
EJEMPLO: ¿Cuanto más rapida sería una maquina con una caché perfecta (sin fallos)?
Programa P:
36% de las instrucciones acceden a memoria.
Frecuencia del reloj 50Mhz
CPIreal = CPIideal + TFi*PFi + D/I*TFd*PFd
CPIreal = 2 + (40*2/100) + (36/100 * 4/100 + 40) = 3.376ciclos.
CPIreal/CPIideal = 1.69. La maquina con caché perfecta funcionaría 1.69 veces más rápido.
¿Que ocurriría si el procesador es mas el mismo pero la memoria mas pequeña?
Si suponemos que el CPIideal es 1 ciclo, entonces el sistema con caché perfecta sería 2.376, que tras la division, es 2.376 veces mas rapido que una maquina sin caché perfecta.
No obstante, habría un tiempo mayor para tratar los bloqueos de memoria.
1.376/3.376 = 41%
1.376/2.376 = 58%
¿Que ocurriría si la memoria fuera igual pero el procesador funcionara mas rapido?
Si se dobla la velocidad del procesador a 100Mhz, manteniendo el CPIideal = 2 ciclos.
El tiempo de acceso no varía pero los ciclos de reloj serían el doble de rapidos que antes, asi que haría 80 ciclos donde antes hacía 40.
CPIreal = 2 + (2/100 * 80) + (36/100 * 4/100 * 80) = 4.75 ciclos.
CPIreal/CPIideal = 1.42 -> No es mejor que la anterior.
¿Y si hubieran cachés de diferentes niveles?
Cachés de primer nivel separadas para datos e instrucciones.
Tiempo de acceso a la memoria principal de 200 ns.
Tasa de fallos en la caché de primer nivel de instrucciones del
Tasa de fallos en la caché de primer nivel de datos del 10%.
Una de cada 4 instrucciones son accesos a memoria.
Maquina con 1 nivel de cache: La penalizacion por fallo es de 100 ciclos:
CPIreal = 1 + (5/100 * 100) + (25/100 * 10/100 * 100) = 8.5
Maquina con 2 niveles de caché: Para acceder a la cache secundaria se necesitan 10 ciclos, la penalizacion por fallo es 12 ciclos.
CPIreal = 1 + (5/100 * 12) + (25/100 * 10/100 * 12) = 1.9
CPIreal-mono-nivel/CPIreal-duo-nivel => La Caché con dos niveles funciona 4.47 veces mas rapido.