A día de hoy, la memoria caché es un tipo de memoria que es clave para el rendimiento de cualquier sistema informático, ya que sin ella la capacidad de trabajo tanto de las CPU como de las GPU caería totalmente en picado. Pero, ¿en qué se diferencia de la memoria RAM convencional, cuál es su utilidad y cómo funciona? Para responder a estas preguntas, os hemos preparado este artículo, para que podáis comprender su importancia y funcionamiento.
¿Qué es la memoria caché?
La caché se sitúa en la jerarquía de memoria entre la CPU y la memoria RAM, pero al mismo tiempo es más lenta que los registros. En la actualidad se encuentra dentro de los microprocesadores, ya sean estos una CPU, GPU o cualquier otro tipo que requiera de caché para funcionar al máximo rendimiento posible. Y es que a medida que fue avanzando a velocidad de vértigo la capacidad de procesamiento de la CPU, el rendimiento de la RAM empezó a no poder seguir el ritmo, lo que se convierte en un cuello de botella enorme.
Y es en este punto donde entra la memoria caché, la cual está pensada para tener una pequeña copia local de los contenidos que está recorriendo el microprocesador en cada momento. No hemos de olvidar que lo habitual es que los programas se ejecuten de forma secuencial, a no ser que haya un salto a otra dirección de memoria, lo cual cambia el contenido del registro del contador de programa.
La idea de la caché es simple: acceder a los datos de la RAM mucho más rápido, sin vernos limitados por la limitación en ancho de banda y latencia de esta. Por desgracia, lo ideal sería que toda la memoria RAM estuviese dentro del chip que ha de procesar los datos, pero eso es imposible y si lo fuera no sería necesaria la memoria caché. En todo caso, su principal ventaja es que su mecanismo es automático, ya que el programa no debe preocuparse de hacer copias de datos y asignar y liberar memoria. Lo cual tiene sus ventajas y desventajas, como que ciertos datos se salgan de la caché y terminen en el siguiente nivel o incluso hasta en la memoria RAM.
Línea de caché
A ojos del microprocesador la memoria caché se organiza de forma distinta a la memoria, en vez de hacerlo en palabras de 8, 16, 32 o 64 bits dependiendo de la arquitectura, la unidad de almacenamiento mínima en una memoria caché es la línea de caché y su tamaño se mantiene consistente en toda la arquitectura. En su interior no solo contiene los datos copiados desde la RAM, sino que también información adicional para su correcto acceso y para que mantengan en todo momento la información correcta (coherencia), ya que de lo contrario podría acarrear problemas.
- Etiqueta (Tag): contiene una parte de la dirección de memoria original de la RAM de donde se copió la línea de caché. Se utiliza para identificar la correlación entre el contenido de la dirección de memoria y los datos almacenados en la línea de caché, y, por lo tanto, determina si los valores son válidos y se pueden usar.
- Bit de validez: indica que la línea de caché contiene datos válidos. Y, por lo tanto, sí se puede acceder a ella para leer o modificar la información
- Bit de suciedad: se activa si los datos han sido modificados en la caché y aún no se han escrito de vuelta a la memoria principal.
- Bits de estado: son indicadores que muestran el estado de cada línea de caché, como si está válida, modificada o en espera de ser escrita en la memoria principal.
Estado | Bit de validez | Bit de suciedad | Descripción |
---|---|---|---|
Modificado (M) | 1 (valido) | 1 (sucio) | La línea ha sido modificada y no coincide con la memoria principal. Solo existe en esta caché y debe escribirse en memoria antes de ser reemplazada. |
Propietario/Owner (O) | 1 (valido) | 1 (sucio en terminos de memoria, pero no necesita corregirse en la RAM de inmediato) | Similar a Compartido/Shared (S), pero esta caché es responsable de proporcionar datos a otras cachés. La memoria principal no tiene la copia más actualizada, pero el dato se puede obtener de esta caché sin escribirlo de vuelta a memoria. |
Exclusivo (E) | 1 (valido) | 0 (limpio) | La línea está solo en esta caché y es idéntica a la memoria principal. Puede modificarse sin invalidar otras copias. |
Compartido/Shared (S) | 1 (valido) | 0 (limpio) | La línea está en múltiples cachés y es idéntica a la memoria principal. No puede modificarse sin invalidar otras copias. |
Adelantado/Forwarder (F) | 1 (valido) | 0 (limpio) | Similar a Compartido/Shared (S), pero esta caché es la encargada de enviar los datos a otras cachés que los soliciten, evitando tráfico innecesario en el bus de memoria. |
Invalido (I) | 0 (invalido) | Irrelevante | La línea no contiene datos válidos. |
Mecanismos de coherencia
Hay una relación directa entre los valores de los bits de validez y suciedad con los bits de estado respecto a mantener el contenido en la memoria caché totalmente coherente. Esto quiere decir que si se modifica la copia en la caché, entonces el sistema ha de tener la forma de actualizar todas las copias existentes en el resto de cachés del microprocesador y en la propia memoria principal del sistema y viceversa. Es decir, la modificación de una información en la RAM debería tener como consecuencia una actualización de todas las copias en caché de dicha dirección de memoria.
Mecanismo | Descripción |
---|---|
MESI | El mecanismo para coherencia de la memoria caché más usado. |
MOESI | Es una extensión de MESI que agrega el estado Owner (O), optimizando el uso del bus. |
MSI | Es el más simple, con solo tres estados. Al carecer de estado "Exclusive" termina por generar más tráfico al acceder a la memoria principal. |
MESIF | Mejora del mecanismo de coherencia MESI, añade el estado F |
Búsqueda de datos en la caché
La idea general de la memoria caché es permitirle al microprocesador, ya sea una CPU o una GPU, acceder a la información en la RAM con el menor tiempo posible (menos latencia) y con un ancho de banda mucho más alto. Por desgracia, no se puede colocar físicamente toda la memoria en la caché, sino que se copia la información más cerca a la dirección o direcciones (si hablamos de un sistema multinúcleo) que se está utilizando en ese momento.
En general, pueden pasar dos cosas cuando se busca un dato en la memoria caché.
- La caché verifica si el dato ya está allí (Cache Hit). Si es así, se entrega al procesador rápidamente.
- Si no está (Cache Miss), se carga el bloque de memoria correspondiente desde la RAM.
Sin embargo, el hecho de que la línea de memoria que busca el microprocesador no se encuentre en un nivel de la caché supone volver a empezar de nuevo en el siguiente nivel y dicho tiempo de transición supone una latencia adicional. En el diseño de nuevas arquitecturas se tiene muy en cuenta el tamaño de cada caché dado que cuanto más grandes son, más tarda en recorrerse y los saltos de nivel que añaden latencia. El propósito es que la caché nunca puede tener una latencia mayor que lo que le costaría a la CPU o la GPU acceder a la memoria RAM.
Políticas de reemplazo
Cuando no se encuentra la información en ninguno de los niveles de la memoria caché (cache miss) es cuando toca realizar un reemplazo que consiste en buscar dicha información en la RAM u otro dispositivo en la jerarquía de memoria y copiarlo en la caché, habitualmente en el último nivel dado que es el más grande de todos. Dependiendo de la arquitectura, a la hora de reemplazar la información en las líneas de caché se utilizará una de estas políticas de reemplazo.
- LRU (Least Recently Used): Se reemplazan los datos que no han sido usados recientemente.
- FIFO (First-In, First-Out): Se reemplazan los datos que llevan más tiempo en la caché.
- LFU (Least Frequently Used): Se reemplazan los datos menos utilizados.
- Aleatorio: Se reemplazan datos de manera aleatoria (rara vez se utiliza, pero puede ser útil en ciertos contextos).
Las líneas de caché se limpiarán de forma inmediata cuando el «bit sucio» esté activo y corresponda con que hay una discrepancia entre el contenido de la RAM y la linea de caché. En ese caso no es un reemplazo, sino más bien una actualización de su contenido.
Niveles de memoria caché y jerarquía de memoria
La memoria caché se encuentra organizada por niveles, donde el nivel más bajo es el más cercano al procesador y el más bajo, el más lejano, pero, al mismo tiempo, el más cercano a la memoria. Los niveles se recorren todos hasta que se encuentra la copia del contenido de la dirección de memoria (cache hit) y se devuelve la información correspondiente. Por lo que con cada nivel adicional el estrés sobre la misma es cada vez menor y la posible tasa de aciertos crece.
Todos los niveles de caché, excepto el más cercano al microprocesador, guardan en su interior una copia exacta del contenido de la RAM, excepto el nivel más bajo donde la caché toma una organización Harvard en vez de Von Neumann donde la memoria caché se clasifica en dos tipos, datos de instrucciones. No obstante, no es el único tipo de caché anómala o fuera de lo común, ya que no podemos olvidar la μ(micro)op caché y lo que es una caché víctima.
Memoria caché de instrucciones
Esta caché almacena las instrucciones más frecuentes; sin embargo, su concepto es algo confuso para el usuario de a pie, digamos que existen instrucciones que por su naturaleza tardan muchos menos ciclos en ejecutarse que otras por el hecho de que se ejecutan de forma directa desde la caché de instrucciones sin tocar la de datos y el resto de niveles. ¿Y qué tipos de instrucciones almacena esta caché y cuáles no?
- Instrucciones de operando inmediato: incluyen un valor constante dentro de la propia instrucción. Esto significa que el operando no se encuentra en la memoria ni en un registro, sino que está codificado en la instrucción misma.
- Instrucciones sin operando (implícitas): instrucciones que no necesitan operandos explícitos porque los valores involucrados están definidos por la propia arquitectura del procesador.
El hecho de separar el primer nivel de la memoria caché entre instrucciones y datos al mismo tiempo, es que esto le permite tener dos buses diferenciados entre datos e instrucciones, lo que les permite a las unidades Load/Store buscar y captar datos en memoria sin obstaculizar el trabajo de ciertos componentes clave en la captación de instrucciones. Dicho de otra forma, si el primer nivel de caché fuese unificado como los demás, entonces el rendimiento bajaría en picado al cortarle el acceso al microprocesador a la jerarquía de memoria, lo cual haría que estuviese parado la mayor parte del tiempo.
Versus caché de datos
El primer tipo de memoria caché que se incluyó en las CPU para PC fue la de instrucciones, en microprocesadores como el 80386 de Intel o el 68030 de Motorola. El motivo de ello es que es mucho más fácil de implementar respecto a la de datos. Esto se debe a que la caché de instrucciones es de solo lectura, dado que las instrucciones son captadas de forma secuencial por la CPU, pero nunca se modifican. Pensad que cambiar el valor del opcode de una instrucción la convierte en una cosa totalmente distinta. Además, esto hace que la caché de instrucciones no haya de ser coherente con el contenido de la información del resto de los niveles de la caché. No olvidemos que es una memoria caché privada a nivel de cada núcleo.
Hemos de partir del hecho de que la caché de instrucciones está optimizada para un acceso secuencial y no aleatorio, debido a que es así como funcionan los programas. Si bien existen instrucciones de salto, estas no suelen ser las más comunes. No olvidemos que las líneas de caché almacenan varias instrucciones de golpe, lo que les permite captar varias instrucciones secuenciales de una tirada, lo que no solo reduce el número de accesos a la memoria, sino que existen altas probabilidades de que la siguiente instrucción se encuentre después de la actual.
Cuando ocurre un salto y la CPU se pone a leer desde otra dirección de memoria, es cuando la caché de instrucciones puede no acertar y va a necesitar recorrer el resto de niveles de la caché para encontrar la instrucción y su dato. Una vez captada, se copiará en la caché de instrucciones de primer nivel para reutilizarla. No obstante, las CPU contemporáneas contienen mecanismos para evitar este problema y permitir que la caché de instrucciones funcione de forma secuencial.
μop caché
La llamada caché de microoperaciones no se encuentra dentro de la jerarquía de caché, ni forma parte de ningún nivel, aunque dependiendo de la arquitectura suele ser llamada caché L0 de instrucciones o caché paralela de instrucciones. ¿Su trabajo? Almacena microoperaciones (μops) decodificadas previamente, durante la etapa de decodificación, con el objetivo de reducir el tiempo por instrucción al evitar la necesidad de decodificar instrucciones repetitivas provenientes de la memoria o de la memoria, caché de instrucciones.
Su presencia en las CPU es relativamente reciente, ya que empezó a implementarse por primera vez en la arquitectura Intel Sandy Bridge (2011) y en el caso de AMD no se estrenó hasta el lanzamiento de la primera generación de su ya más que aclamada y consolidada arquitectura Zen. Por lo que se trata de uno de los elementos clave para mejorar el rendimiento de los programas en los microprocesadores más actuales. Entre las ventajas que aporta, las que más destacan son:
- El tener almacenadas las μops ya decodificadas reduce la carga de trabajo del decodificador de instrucciones, y acelera la ejecución de otras instrucciones, evitando que se gasten ciclos de reloj de forma repetitiva, al mismo tiempo que reduce el consumo energético.
- Cuando un bucle se ejecuta repetidamente, las instrucciones ya decodificadas pueden ser recuperadas directamente desde el μop caché.
- Si hablamos de un microprocesador con ejecución fuera de orden, entonces la μop caché facilitará una recuperación más rápida de instrucciones.
En todo caso, hay que aclarar que, a la hora de buscar un dato a memoria, en ningún momento se visita esta caché. Su utilidad es más bien para acelerar las etapas de decodificación de instrucciones.
Victim Caché
En algunas arquitecturas, ciertos niveles de caché más alejados del microprocesador sirven como basurero para otros niveles de caché más cercanos al procesador. Para entenderlo, hemos de partir del hecho de que, cuando se produce un fallo en la búsqueda del dato (cache miss) y a posteriori y se copia luego una información desde la memoria RAM a la memoria caché siempre se copia primero en el nivel más alto y luego se va copiando en el resto de niveles según la localidad. Entiéndase como la distancia en direcciones de memoria respecto a donde apuntan los registros de contador de programa en ese momento.
Pues bien, cuando hay una caché víctima como caché de último nivel, esta es ignorada a la hora de copiar los datos desde memoria, y solo se llenará cuando por políticas de reemplazo la información se elimine del nivel de caché inmediatamente más bajo. En este caso no se volcará a memoria, sino que se copiará en la caché víctima, la cual en la búsqueda de datos sí que funcionará como un nivel adicional en la jerarquía de memoria. Un ejemplo claro de este tipo de caché es la caché de tercer nivel en las CPU AMD Ryzen de AMD y la Infinity Cache de sus GPU RDNA 2 en adelante para PC, las cuales heredan por completo los mismos mecanismos que los microprocesadores de la misma marca.