Jerarquía de memoria

No se puede conseguir una memoria muy rápida y muy grande al mismo tiempo, por eso se usan varias memorias de distintos tamaños y velocidades. Las memorias usadas son, en orden de rapidez: registros del procesador, caché (de distintos niveles: L1, L2,...), memoria principal (RAM), memoria secundaria (disco duro), memorias externas (CDs, pendrives,...).

Los datos solo se mueven de un nivel de memoria a su anterior o posterior.

## Principio de localidad

La localidad de las referencias, también conocida como el principio de localidad, es un fenómeno según el cual, basándonos en el pasado reciente de un programa podemos predecir con una precisión razonable qué instrucciones y datos utilizará en un futuro próximo.

Los casos más importantes de localidad son:

* **Localidad temporal**: Un mismo dato se usa varias veces en poco tiempo.
* **Localidad espacial**: Datos contiguos suelen ser usados casi al mismo tiempo.

La idea es que en el disco duro esté todo lo que el programa pueda llegar a necesitar, pero solo las partes que está usando en un instante dado estén en la memoria principal (estas partes se llaman páginas), y a su vez en la caché solo está las partes de la memoria principal que hacen falta en ese instante (estas partes se llaman bloques).

Cada división o agrupación de partes de memoria tiene que ser en potencias de 2. Esto simplificará mucho las cosas más adelante.

## Memoria caché

Toda la gestión de la memoria caché se hace mediante controladores hardware.

La memoria principal y la caché se dividen en **bloques** (a los bloques de la caché también se les llama **líneas**). Cuando la CPU necesita un dato de memoria principal trae a caché el bloque entero (a menos que ya lo haya hecho antes).

Las líneas se agrupan en **conjuntos** (todos con la misma cantidad de bloques). A cada bloque de memoria principal le corresponde un único conjunto de la caché. Evidentemente a muchos bloques les corresponderá el mismo conjunto, pues en cada conjunto caben varios bloques y la caché es más pequeña.

### Políticas de emplazamiento

La cantidad de conjuntos, y por tanto, la cantidad de bloques en cada conjunto determina la política de emplazamiento:

* **Emplazamiento directo**: conjuntos con una sola línea.
* **Emplazamiento totalmente asociativo**: un solo conjunto.
* **Emplazamiento asociativo por conjuntos**: más de un conjunto de más de una línea. Al número de líneas en cada conjunto se le llama número de vías o grado de asociatividad.

### Políticas de reemplazamiento

Al traer a caché un bloque este se tendrá que guardar en una de las líneas del conjunto que le corresponde. Hay varias formas de decidir en cuál de esas líneas guardarlo, estas se llaman políticas de reemplazamiento:

* **Aleatorio**.
* **FIFO**: se quita el bloque del conjunto que más tiempo lleva en caché, dejando sitio para el nuevo.
* **LRU (less recently used)**: se quita el bloque que lleva más tiempo sin usarse.
* **LFU (less frequently used)**: se quita el bloque que se usa con menos frecuencia.

La información necesaria para implementar FIFO, LRU, LFU se guarda en unos bits de referencia asociados a cada bloque.

### Políticas de actualización

Para saber qué bloques de memoria principal hay en caché se les etiqueta. En caché, junto con el bloque, se guarda su etiqueta. Bloques a los que les corresponden distintos conjuntos pueden tener la misma etiqueta.

Para saber qué líneas tienen contenido válido y en qué líneas aún no hay nada, hay un bit de validez por cada línea. Al encender el PC no hay ninguna línea válida.

Para asegurarse de que los datos de la caché y de la memoria principal son consistentes hay varias políticas de actualización:

* **Write-through**: todas las escrituras se hacen tanto en caché como en memoria principal. Así la memoria principal siempre está actualizada.
* **Write-back (post escritura)**: las escrituras se hacen en caché y cuando terminamos con el bloque hay que copiarlo en memoria principal si ha sido modificado. Usaremos un dirty bit o bit de sucio para saber si se ha escrito en el bloque o no.

También forma parte de la política de actualización el decidir que hacer al escribir en un bloque que no está en caché (write miss):

* **Write allocate**: se trae el bloque a la caché antes de escribir en él.
* **No-write allocate**: se escribe directamente en memoria principal sin traer el bloque.

### Direcciones

Para averiguar qué conjunto y qué etiqueta le corresponde a un bloque de memoria principal basta con mirar la dirección de cualquier dato que contenga.
Si la memoria principal tiene

2
n

{\displaystyle 2^n}

 posiciones de memoria (

n

{\displaystyle n}

 bits de dirección), bloques de

2
k

{\displaystyle 2^k}

 posiciones y la caché tienen

2
r

{\displaystyle 2^r}

 conjuntos. Entonces las direcciones se descomponen así:

[PICT.]

Por tanto al acceder a una dirección de memoria lo que ocurre es:

1. Se busca la etiqueta entre las líneas del conjunto correspondiente.
2. Si se encuentra la etiqueta, se accede.
3. Si no se encuentra, se busca el bloque en memoria principal y se trae al conjunto que le corresponde, quitando otro bloque según la política de reemplazamiento.

### Rendimiento

Llamamos **hit o acierto** a que el dato buscado esté en caché. Si no está es un **fallo o miss**. Con esto, se definen los siguientes conceptos referentes al rendimiento de un computador:

* **Tasa de aciertos**: nº aciertos entre el nº de accesos a memoria
* **Tasa de fallos**: nº de fallos entre el nº de accesos a memoria
* **Tiempos de acierto**: el que se tarda en traer un dato que está en caché.
* **Tiempo de penalización por fallo**: el tiempo que se tarda de más cuando hay fallo que cuando hay acierto.

Se definen tres tipos de fallos:

* **Iniciales**: la primera vez en traer un dato habrá un fallo siempre. Cuanto más grandes sean los bloques menos ocurrirán.
* **Capacidad**: no caben todos los datos en caché y se descarta de uno que luego vuelve a usar. Con bloques más pequeños o caché más grande pasa menos.
* **Conflicto**: no caben todos los bloques en el conjunto y se descarta uno que luego se vuelve a usar. Cuanto más grandes sean los conjuntos menos pasará.

Idealmente cada proceso tarda:

T

c
p
u

=
#
i
n
t
r
u
c
c
i
o
n
e
s
×
C
.
P
.
I
.
×

T

c
i
c
l
o

{\displaystyle T\_{cpu}=\#intrucciones\times C.P.I.\times T\_{ciclo}}

Pero la realidad es que hay que añadir la espera a memoria. (Se entiende que el tiempo de acierto no añade espera).

**Error al representar (error de sintaxis): {\displaystyle T\_{cpu} = \# intrucciones \times C.P.I. \times T\_{ciclo} + \# fallos \times T\_{penalización} }**
**Error al representar (error de sintaxis): {\displaystyle T\_{cpu} =  \# intrucciones \times C.P.I. \times T\_{ciclo} + \# accesos\\_mem \times tasa\\_fallos \times T\_{penalización} }**
**Error al representar (error de sintaxis): {\displaystyle T\_{cpu} =  \# intrucciones \times C.P.I. \times T\_{ciclo} + \# intrucciones \times \% accesos\\_mem \times tasa\\_fallos \times T\_{penalización} }**

### Mejoras de rendimiento

Para mejorar el rendimiento se pueden hacer muchas cosas:

* Optimizar el código para que cumpla mejor el principio de localidad.
* Pre-búsqueda: traer bloques que probablemente vayan a ser usados.
* Caché de víctimas: caché pequeña, totalmente asociativa, en la que se guardan los bloques recién descartados de la caché principal.
* Caché multinivel: usar varias cachés cada una más grande y lenta que la anterior.
* Escribir en memoria mientras el proceso continúa.
* Caché pequeña y sencilla para accesos más rápidos.

## Memoria virtual

La gestión de memoria virtual se hace entre módulos hardware y el SO.

La memoria virtual es la que se le hace creer a cada proceso que tiene disponible. Si el procesador genera direcciones de

n

{\displaystyle n}

 bits entonces hay

2
n

{\displaystyle 2^n}

 direcciones virtuales disponibles. Normalmente todas ellas no caben en la memoria principal, y más teniendo en cuenta que varios procesos pueden estar usando la memoria principal simultáneamente.

Para sortear estas dificultades se guarda en memoria secundaria (disco duro) todo lo que necesita el programa y se divide en **páginas virtuales** del mismo tamaño (

2
k

{\displaystyle 2^k}

). Así mismo la memoria principal se divide en **marcos de páginas** o **páginas físicas** donde se guardarán las páginas virtuales.

### Dirección virtual y dirección física

Tendremos direcciones virtuales (las que el proceso cree que son) y direcciones físicas (las direcciones reales de memoria principal en las que se guardan).

Direcciones virtuales:

[Pict.]

Direcciones físicas (memoria principal de 2m palabras, m<n):

[Pict]

### Emplazamiento y reemplazamiento

Aquí los fallos de página (que la página buscada no esté en memoria principal) tienen una enorme penalización, así que para disminuirlos se utiliza un **emplazamiento totalmente asociativo**, es decir, cualquier página virtual puede ir a cualquier marco de página. Como política de actualización se usa **write-back**.
Como política de **reemplazamiento** el SO utiliza algoritmos (LRU, LFU,...) para elegir la página que más vaya a tardar en volver a ser usada.

### Tablas de páginas

El problema de usar emplazamiento totalmente asociativo es que se tarda mucho en averiguar en qué marco de página está la página virtual buscada. Por ello se usan **tablas de páginas**: estructuras de datos que a cada página virtual le asigna un marco de página.

Cada proceso tendrá que tener su tabla de página. Esto hace que las tablas ocupen mucho espacio, por ello se hacen tablas de tablas: tablas que dada una dirección virtual te dicen donde está la tabla que sabe que dirección física le corresponde.

Las tablas pueden contener información extra en **bits de control**, como un bit de validez, bit de dirty, bits de seguridad (permisos que tiene el proceso sobre la página),...

Para acceder a datos de caché hace falta la dirección física que está en memoria principal. Esto se soluciona añadiendo una caché específica para tablas de páginas: la **TLB** (Translation Lookaside Buffer), que tiene las direcciones físicas de las páginas de las que hay bloques en caché. También tiene que incluir los bits de control de tabla (validez, dirty, seguridad,...). También se pueden añadir identificadores de proceso, que indican a qué proceso le corresponde cada traducción, para no cambiar la tabla por completo cada vez que se cambia de proceso.

### Integración con caché

Teniendo memoria virtual hay varias formas de organizar la caché:

* **Caché física**: Usando la dirección física para determinar el conjunto y la etiqueta. Hay que traducir en la TLB la dirección virtual antes de ir a la caché.
* **Caché virtual**: Usando la dirección virtual para determinar el conjunto y la etiqueta. Solo se traduce la dirección para acceder a la memoria principal.
* **Caché virtualmente accedida, físicamente marcada**: Básicamente es como la física, pero se usa la parte de la dirección que no cambia (el desplazamiento) para ir accediendo al conjunto mientras que se traduce el nº de página. Cuando se tiene el nº de página física se usa para determinar la etiqueta y averiguar en qué línea del conjunto está el bloque buscado.

Para esto el desplazamiento de página tiene que ser mayor o igual en tamaño que el de bloque y el conjunto juntos.

[Pict.]