Descarga la aplicación para disfrutar aún más
Vista previa del material en texto
La cache, base de la jerarquía de memoria Manuel Alejandro Barranco González Iván Guardia Hernández 1.- Introducción Una de las partes fundamentales de una computadora es su sistema de memoria. El rendimiento de ésta condicionará las prestaciones del resto del sistema. Hoy en día podemos escoger entre muchos tipos de memoria distintos. Podemos elegir entre aquellas que son rápidas y pequeñas y las que son lentas pero con gran capacidad de almacenamiento. Interesa tener un sistema de memoria de tamaño grande y que permita acceder a la información de forma rápida. Todo esto nos conduce a organizar diferentes tipos de memoria de forma que podamos obtener un mayor rendimiento. ¿Cómo? Los datos que se requieren durante la ejecución de un programa suelen estar ubicados en una misma región de memoria y, además, suelen ser accedidos muchas veces en un intervalo de tiempo corto. Estos resultados se conocen con los nombres de principio de localidad espacial y principio de localidad temporal. Siguiendo estos principios, la memoria se construye jerárquicamente, de forma que las partes más rápidas se encuentran más cerca del procesador. A medida que bajamos de nivel en la jerarquía, perdemos velocidad pero ganamos capacidad de almacenamiento. De esta forma, se consigue que todo el sistema de memoria tenga un rendimiento próximo al de su parte más rápida. Básicamente, podemos dividir la jerarquía en tres niveles. El primero de todos, y sobre el que se apoya el procesador, está formado por la memoria cache. Ésta posee una velocidad elevada, pero tiene poca capacidad. El siguiente nivel está formado por la memoria principal, mucho más grande, pero de menor velocidad. El último lo constituye la memoria secundaria, de una capacidad enorme pero muy lenta. Este último nivel se gestiona y relaciona con el resto de la jerarquía para conseguir de forma virtual una memoria de gran capacidad y de velocidad próxima a la cache. ¿Qué es lo que se debe tener en cuenta en la jerarquía para que todo el sistema de memoria tenga un rendimiento próximo al de la cache? La información de cada nivel está organizada en forma de bloques. Cuando se accede a un nivel de memoria para obtener un dato, éste puede tenerlo o no. Si lo tiene ha habido un acierto, y si no lo tiene ha habido un fallo, en cuyo caso hay que ir al nivel inferior para recuperar el bloque que contiene el dato. La frecuencia de aciertos es la fracción de accesos a memoria encontrados en el nivel superior, mientras que la frecuencia de fallos es la fracción de accesos no encontrados. Ambos se suelen representar con un porcentaje. El tiempo de acierto esta formado por el necesario para determinar que a habido un acierto, más el tiempo en acceder al dato en el nivel superior. La penalización de fallo es el tiempo necesario para traer un bloque del nivel inferior al superior, dividiéndose en tiempo de acceso y transferencia. Por tanto, los niveles inferiores a la cache deben ser diseñados de manera que permitan reducir la penalización de fallo y el tiempo de acierto de ésta. En primer lugar describiremos la estructura básica de la cache con el fin de profundizar más en su funcionamiento, así como los distintos aspectos que hay que tener en cuenta para obtener un buen rendimiento de la misma. Posteriormente analizaremos la organización de la memoria principal y las técnicas de memoria virtual que se pueden utilizar para conseguir nuestro objetivo, aproximar el rendimiento del sistema de memoria al de la cache. 2.- Memoria cache La cache es el primer nivel dentro de la jerarquía de memoria, situándose bajo el procesador. Hoy en día la utilización de esta memoria se ha hecho casi indispensable para cualquier computadora, pudiendo utilizar en muchos de ellos varias memoria cache. Después de los registros del procesador, es el tipo de memoria más rápido, pero a su vez es el que dispone de menos espacio donde poder guardar la información. Por ello, para conseguir aumentar el rendimiento del sistema utilizando la cache se coloca la información que más se utilizará en ella. Como a este nivel los accesos a memoria son más rápidos, la información podrá ser obtenida antes. Al disponer de menos memoria cache que la que hay en niveles inferiores de la jerarquía, mucha información tendrá que ser buscada fuera de ella. El bloque de información que contiene el dato que provocó el fallo debe ser cargado en la cache sobre otro bloque ya situado en ella. El bloque seleccionado para ser reemplazado debe ser escrito en el siguiente nivel, siguiendo una estrategia determinada. Como el acceso a un nivel inferior supone una pérdida de tiempo o penalización de fallo, es muy importante tener en cuenta cual es la frecuencia de acierto, es decir, la probabilidad de encontrar la información en la cache. Para aumentar esta tasa de acierto se debe encontrar la forma de ubicar y reemplazar la información en la cache que aproveche lo mejor posible el principio de localidad espacial. Pero no solamente es importante construir la cache de forma que se incremente la frecuencia de acierto. También es necesario conseguir que el tiempo de acierto y la penalización de fallo sean lo más pequeños posible. Desde el punto de vista de la cache se pueden conseguir estos objetivos mediante una organización que permita determinar rápidamente si un bloque de información se encuentra en la cache o no, y mediante una política de escritura de los bloques en los niveles inferiores a ella que sea acertada. 2.1.- Como podemos organizar la memoria cache Una dirección de memoria generada por un programa esta compuesta por un número de página, seguido de un desplazamiento; con el numero de página obtenemos el marco de memoria correspondiente a través de una tabla de páginas, para luego sumarle el desplazamiento y así obtener la dirección física. En la cache, la dirección de memoria física la dividimos en tres partes: una etiqueta para identificar los bloques, un índice que ayudará a localizar y colocar los bloques, y por ultimo un desplazamiento de bloque que se usa para seleccionar el dato deseado del bloque. Internamente cada bloque que se encuentra dentro de la cache esta compuesto por los datos, la etiqueta que lo identifica, y además un bit de validez que nos sirva para saber si los datos que hay se encuentran son válidos. Como veremos más adelante, hay otro bit de modificación que permite saber si los datos han sido cambiados mientras estaban en la cache. Básicamente podemos organizar la memoria cache de tres formas diferentes: correspondencia directa, asociativa por vías y completamente asociativa. 1.- Correspondencia directa: Cada bloque tiene un solo lugar donde puede estar dentro de la memoria. La búsqueda del bloque se basa en localizar el bloque dentro de una única tabla a través del índice de la memoria física, luego se compara la etiqueta saliente de la cache con la etiqueta de la memoria física, y si coinciden ya tenemos los datos solicitados. A continuación mostramos un esquema que ilustra este tipo de organización. 2.- Asociativa por vías: Cada bloque se puede colocar en un conjunto restringido de lugares de la cache. Un conjunto es un grupo de dos o más bloques dentro de la cache. En concreto si hay N bloques organizados en la cache se dice que es una cache asociativa de N vías. A continuación se muestra un ejemplo con 2 vías, donde se busca en paralelo la etiqueta en ambos bloques. Luego, con un pequeño circuito se determina en que bloque se acertó, y así se escogen los datos correctos a través de un multiplexor: Seguidamente mostramos un esquema para clarificar este tipo de organización. etiqueta índice desplazamiento etiqueta datos =? acierto/fallo CPU 3.- Completamente asociativa: Cada bloque se puede colocar en cualquier lugar de la cache. En concreto, podemos decir que es un caso de memoria asociativa con el máximo numero de vías posibles. Es decir, existe un solo conjunto que posee todos los bloques. Por tanto no existeel campo de índice y sólo usamos la etiqueta. Para encontrar el bloque buscado se comparará en paralelo la etiqueta de la dirección física con todas las etiquetas de cada conjunto. etiqueta índice desplazamiento etiqueta datos =? CPU =? 2.2.- Política de reemplazado en un fallo de cache Cuando se intenta acceder a un bloque que no se encuentra en la cache hay que ir a buscarlo al nivel inmediatamente inferior, y posteriormente colocarlo en la cache para los futuros accesos. En este momento hay que decidir que bloque quitamos para colocar el nuevo. Es muy importante la política de reemplazo, ya que de ella dependerá la tasa de aciertos. Si usamos una correspondencia directa no hay elección, cada bloque solo puede ser ubicado en un sitio. De esta manera se simplifica mucho el hardware adicional que tenemos que poner. Si de los bloques candidatos hay alguno que tiene el bit de no válido, entonces este ya será la víctima. El problema viene cuando todos son válidos, entonces tenemos que adoptar alguna política de reemplazo, en concreto hay tres maneras de escoger la víctima: 1.- Aleatorio: Simplemente escogemos un bloque de forma pseudoaleatoria. De esta manera se simplifica el hardware que tenemos que añadir. 2.- Menos recientemente utilizado: Se basa en la localidad temporal de los datos, donde cogemos el que hace más que no se usa. Se requiere un control del tiempo de acceso de cada bloque, esto puede ser muy costoso en hardware y tiempo de selección, pero a cambio no ofrece mejor tasa de acierto. 3.- El primero en llegar es el primero en salir: Los bloques sustituidos van saliendo en el mismo orden en el que llegaron. 2.3.- Escrituras en la cache En los casos de lectura de bloques se podía realizar en paralelo la verificación de la etiqueta y la lectura del dato, así si había acierto ya teníamos el dato y no había tiempo perdido por culpa de la comprobación de la etiqueta. En cambio, en una escritura esto no puede ser así porque sólo se podrá hacer si realmente es el bloque correcto, y no se sabrá hasta pasada la verificación de la etiqueta. Por este motivo, las escrituras suelen ser más lentas que las lecturas. Podemos adoptar dos estrategias para escribir en la cache: 1.- Escritura directa: La información se escribe tanto en la cache como en la memoria principal. 2.- Postescritura: La información se escribe sólo en la cache, y si posteriormente se tiene que reemplazar el bloque, entonces este se escribe en niveles inferiores de la jerarquía antes de ser sustituido. Para llevar a cabo esta estrategia hay que añadir un bit de modificado a cada bloque dentro de la cache, que será el indicativo para saber si tenemos que actualizar el bloque en el nivel inferior a la hora de sustituirlo por otro en la cache. 2.4 Variantes sobre el modelo original Una primera variante consiste en subdividir la cache en dos, una para los datos y otra para las instrucciones. Si se dispone de puertos de memoria separados para ambos, podemos trabajar en paralelo, casi doblando así el ancho de banda entre la cache y la CPU. Además, de esta manera se puede aumentar el rendimiento de ambas por separado, teniendo en cuenta el uso individual que se hará de cada una de ellas. Otra variante se basa en que cada bloque de la cache puede almacenar más datos. Si aumentamos el tamaño del bloque, se aprovecha más la localidad espacial. Pero no hay que descuidar que el incremento del tamaño del bloque supone reducir el número de bloques y por tanto desaprovechar la localidad temporal. Lo ideal sería buscar el tamaño idóneo para maximizar el uso de la localidad temporal y espacial. 3.- Relación entre la memoria principal y la cache La memoria principal se encarga de dar servicio a la cache y a las operaciones de entrada y salida. Es el siguiente nivel después de la cache y por tanto condiciona el rendimiento de ésta. Recordemos que en una jerarquía los niveles superiores se apoyan en los inferiores, de forma que la penalización de fallo en el nivel superior depende del tiempo de acceso del nivel inferior y el tiempo de transferencia entre ambos niveles. Así pues, es importante encontrar una organización adecuada con el fin de reducir ambos tiempos y conseguir una penalización de fallos pequeña para la cache. Normalmente es mucho más fácil reducir el tiempo de transferencia entre los dos niveles de memoria que decrementar el tiempo de acceso. Esto es debido a que es más sencillo aumentar el ancho de banda de la memoria, consiguiendo un incremento de la velocidad de transferencia, que conseguir tecnologías de semiconductores nuevas con un tiempo de latencia más pequeño. Es decir, la forma más fácil de reducir la penalización de fallos de la memoria cache es incrementar el número de palabras por ciclo de reloj que se pueden transferir entre ésta y la memoria principal. Una primera organización que incrementa el ancho de banda de la memoria consiste en aumentar la anchura de la memoria principal, es decir, el número de bytes que proporciona la memoria en cada acceso. De este modo, en un mismo ciclo de reloj se transfiere un mayor número de bytes entre los dos niveles de la jerarquía, reduciendo el tiempo de transferencia. Sin embargo, esta estrategia posee una serie de inconvenientes: 1.- En primer lugar aumenta el coste del bus de datos, puesto que se debe incrementar su anchura para permitir la transmisión de los bytes tras cada acceso a memoria principal. 2.- Existe un conflicto entre los accesos de la CPU a la cache y el ancho de banda de memoria. Normalmente estos accesos poseen el tamaño de una palabra. Como la memoria cache debe poder soportar un ancho de banda mayor, ha de existir un punto de multiplexación entre la memoria principal y la CPU, provocando la aparición de retardos. Es decir, la cache debe poder leer y escribir datos en el bus de forma que se pueda realizar la transferencia respetando el ancho de banda requerido. Una primera alternativa sería implementar una memoria cache con la misma anchura que la memoria principal. En este caso, el punto de multiplexación se encontraría entre la CPU y la cache. Pero esta organización es la que provoca la aparición de retardos mayores. Por ello y siempre que la cache sea lo suficientemente rápida en comparación con el bus, se puede implementar ésta con anchura de una palabra y conectar el multiplexor entre el bus y la cache. A continuación presentamos un esquema en el que se describe este tipo de organización. A la izquierda se ilustra un esquema de memoria ancha en el que el punto de multiplexación se encuentra entre la cache y la CPU, mientras que en la derecha se muestra una organización en la que la cache es mucho más rápida que el bus y el multiplexor se ubica entre ambos. Una segunda organización, conocida como memoria entrelazada, también permite el incremento del ancho de banda. La idea consiste en solapar en el tiempo los accesos que sobre la memoria principal realiza la cache. La memoria principal se divide en distintos bancos. Se incrementa el ancho del bus de direcciones, pero no el ancho de la memoria principal, de la cache ni del bus de datos. Se pueden enviar varias direcciones al mismo tiempo, cada una dirigida hacia un banco en concreto. De esta forma, el tiempo de latencia de un conjunto de accesos es equivalente al de sólo uno, reduciendo el número de ciclos de penalización de fallo de la cache. Tras realizarse los accesos, las palabras son transferidas secuencialmente por el bus de datos entre los dos niveles de la jerarquía. Según la correspondencia o factor de entrelazado entre las direcciones y los bancos, se tienen tipos diferentes de memoria entrelazada. El tipo más utilizado y para el que esta organización fue concebida inicialmente es el de memoria entrelazada por palabras. Para entender el entrelazado por palabras, supongamos que los bancos están numerados desde cero hasta N-1, siendo N el número total de bancos. El banco M contendrá aquellas palabras cuya dirección módulo N sea M.Este tipo de entrelazado permite una mejora importante del rendimiento de la memoria cuando soporta accesos secuenciales, ya que las palabras se encuentran en distintos bancos y un mayor número de ellas pueden ser accedidas simultáneamente. Como los fallos de lectura en cualquier tipo de cache y las escrituras en memorias cache Memoria principal Cache CPU Multiplexor Bus Memoria principal Cache CPU Bus Multiplexor de postescritura se realizan por bloques de palabras consecutivas, se puede sacar partido de este entrelazado. Sin embargo, la memoria entrelazada también posee sus inconvenientes: 1.- La facilidad para poder dividir la memoria en un conjunto de bancos entra en conflicto con la capacidad de los chips de memoria. Cada chip de la memoria está formado por una matriz de celdas, cada una de las cuales capaz de almacenar un bit. Los chips poseen terminales eléctricos que permiten direccionar una fila de la matriz para leer o escribir datos en ella. El número de bits de cada fila puede variar de uno a la longitud total de la palabra (32 bits en la mayoría de computadoras). Cuanto más grandes sean los chips de memoria, es decir, cuantas más filas posea su matriz, menor flexibilidad se tiene para distribuirlos de forma que formen bancos. Supongamos, por ejemplo, que queremos construir una memoria de 16 MB, que la matriz de celdas de los chips posee un bit por fila y que se almacenan palabras de 32 bits. Si los chips son de 256 KB, podemos construir 16 bancos de 32 chips cada uno. Sin embargo, si los chips son de 4 MB, solo podemos construir un banco con 32 chips, desaprovechando memoria de los chips. Como la tendencia es construir chips con mayor tamaño, disponer de una memoria entrelazada es cada vez más costoso, ya que un mismo número de bancos supone un incremento muy grande de la capacidad de la misma. 2.- Normalmente, el hardware de control de los sistemas de memoria entrelazada precisa de bancos del mismo tamaño, por lo que incrementar la memoria supone incrementar el tamaño de todos ellos. Como la diferencia de capacidad entre distintos chips es muy grande, debido a la tendencia a hacerlos cada vez mayores, un incremento mínimo de la memoria supone, en la mayoría de los casos, la duplicación de la misma. A continuación presentamos un esquema en el que se muestra la distribución de un sistema típico de memoria entrelazada. La memoria principal se divide entre cuatro bancos distintos y tanto el bus como la cache tienen la anchura de un banco. 4.- Relación entre la memoria virtual y la cache El siguiente nivel en la jerarquía después de la memoria principal está formado por los discos, es decir, por el almacenamiento secundario. La memoria secundaria es fundamental para permitir la ejecución de múltiples procesos en un sistema. Cada proceso posee un espacio de direcciones sobre el que está trabajando. Sin embargo, a cada uno de ellos se le asigna una cantidad de memoria principal menor a su espacio de direcciones. Para ello ambos se dividen en bloques, de manera que sólo una parte de los que forman el espacio de direcciones se cargan en los de memoria principal. Si un proceso hace referencia a una dirección que no está en ninguno de los bloques de memoria principal que le fueron asignados, reemplaza uno de esos bloques por el que contiene la dirección a la que hizo referencia. De esta forma, el proceso sólo posee en memoria principal los bloques necesarios para su ejecución, mientras que el resto está ubicado en memoria secundaria. Pero el hecho de disponer de memoria secundaria permite, además, que los programas puedan disponer de un espacio de direcciones mayor que el de memoria principal. Disponemos, así, de un sistema de memoria virtual. Para entender la relación de la memoria virtual con la cache debemos tener en cuenta la forma en que se localizan los datos en memoria principal. Existen básicamente dos tipos de memoria virtual. Los sistema paginados son aquellos en los que los bloques son de tamaño fijo, mientras que en los CPU Cache Bus Banco de memoria 0 Banco de memoria 1 Banco de memoria 2 Banco de memoria 3 segmentados son de tamaño variable. Las direcciones virtuales se dividen en dos partes. La parte más significativa de la dirección especifica el número de bloque, página o segmento según el caso, mientras que la menos significativa determina el desplazamiento dentro del bloque. Para realizar la traducción de la dirección virtual a dirección física, el sistema operativo mantiene para cada proceso una tabla de bloques. La tabla se indexa con el número de bloque y para cada entrada posee un bit que determina si éste está cargado o no en memoria principal. En caso de que el bloque esté disponible, también contiene la dirección física del mismo. Ya empezamos a ver cual es el nexo entre la memoria virtual y la cache. Para determinar si existe un acierto o fallo de cache, la dirección debe ser traducida previamente. Las tablas de bloques suelen ser muy grandes y se ubican en memoria principal, incluso pueden estar ellas mismas paginadas o segmentadas. Cualquier acceso a memoria tiene un coste excesivo, ya que debe pasar antes por la tabla y traducirse, produciendo un acceso a memoria principal. La primera solución a este problema consiste en construir una pequeña cache que contenga parte de la tabla de bloques. Confiando en el principio de localidad, se almacenan en ésta pequeña tabla las entradas de la tabla de bloques a las que se han accedido más recientemente. Está cache se conoce con el término de buffer de traducciones anticipadas o TLB. Aunque el uso del TLB suponga un incremento de la velocidad de acceso a la memoria, la comprobación de las etiquetas de la cache debe esperar a que el TLB dé su resultado. Para conocer el valor de acierto de la cache de forma más rápida podemos realizar el acceso al TLB y a la cache de forma simultánea. Para ello, de la dirección que produce la CPU se toma la parte de la etiqueta como el número de bloque, página o segmento, accediendo con ella al TLB. A su vez, se accede a la cache con el resto de la dirección. Como la traducción a través del TLB es más rápida que la obtención de la etiqueta en la cache, cuando se dispone de ésta ya se puede realizar la comparación para determinar el acierto o fallo de cache. Sin embargo, existe una limitación de tamaño para las cache de correspondencia directa. Como el acceso a la cache se realiza con la parte de la dirección que especifica el desplazamiento dentro del bloque donde se encuentra el dato, el tamaño de la misma no puede exceder el tamaño de bloque. Por ello, si se quieren tener memorias cache de mayor capacidad se ha de aumentar la asociatividad y comparar la salida del TLB con las etiquetas de cada una de las vías paralelamente. Una segunda posibilidad es disponer de una cache virtual. En este caso, se accede a la cache con las direcciones virtuales y se comparan las etiquetas directamente sin pasar por el TLB. Sólo se accederá al TLB en caso de que se dé un fallo de cache. Pero este tipo de cache posee dos inconvenientes: 1.- En un entorno de multiprogramación se realizan continuamente cambios de procesos. Esto hace que una misma dirección virtual apunte a zonas de memoria distintas para procesos diferentes. Por ello, la cache debe ser limpiada, purgase, cuando se cambia de proceso. Una solución a este problema se basa en el hecho de que el sistema operativo identifica a cada proceso de forma única con un número en concreto, conocido como PID. Este PID se incluye en las etiquetas de la cache para determinar a qué proceso pertenece cada bloque. De esta forma, no hace falta limpiar la cache con cada cambio de proceso, si bien se debe tener en cuenta que un fallo de cache también puede producirse porque el PID de la etiqueta no se corresponde al del proceso que está accediendo a la misma. 2.- En algunos sistema operativos se permite que una aplicación pueda referenciar el mismo dato con dos direcciones virtuales distintas, conocidas como alias. Los aliaspueden producir la replicación de un mismo bloque en la cache virtual, porque la dirección no está traducida a su valor físico. Existen implementaciones hardware que impiden la duplicación de bloques en la cache, si bien algunos sistemas operativos pueden forzar a que los alias compartan el número de bits menos significativos suficientes para evitar la duplicación de bloques. Por ejemplo, si poseemos una cache de correspondencia directa con un tamaño igual o inferior a 218 bytes, basta que los 18 bits menos significativos de los alias coincidan. A continuación se muestra una gráfica que representa la frecuencia de fallos frente al tamaño de la cache virtual para un sistema de monoprogramación, un sistema de multiprogramación con gestión de PID en la cache y un sistema de multiprogramación con purga. Como se puede observar, para cualquier tamaño gestionar el PID en la cache virtual produce la menor frecuencia de fallos. La purga es un buen método cuando el tamaño de la cache virtual es pequeño, pero su efectividad disminuye ligeramente con el aumento del mismo. Por su lado, cuando se permite la ejecución de un solo proceso, la frecuencia de fallo disminuye con el aumento de la capacidad de la cache virtual, ya que un número mayor de sus bloques estarán ubicados en ella. Estas estadísticas fueron obtenidas tras un estudio realizado por Agarwal en 1987. Se utilizó el sistema operativo Ultrix, corriendo en un VAX y utilizando memorias cache de correspondencia directa con un tamaño de bloque de 16 bytes. 0 5 10 15 20 Frecuencia de fallos % 2 4 8 16 32 64 128 256 512 1024 Tamaño de la cache virtual (KB) Frecuencia de fallos frente a tamaño de cache virtual Purga PIDs Uniproceso 5.- Conclusión La jerarquía es una organización que permite aumentar el rendimiento del sistema de memoria. Se intenta que este rendimiento sea lo más parecido posible al del nivel más rápido. Como el nivel más rápido de todo el sistema después del procesador es la memoria cache, el diseño de las otras partes del sistema de memoria, tales como la memoria principal y la virtual, debe realizarse de tal forma que aumente el rendimiento de la misma. Para ello se buscan organizaciones de las distintas partes de la jerarquía que permitan reducir la penalización y el tiempo de acierto de la cache.
Compartir