<< Chapter < Page Chapter >> Page >

Las líneas de cache pueden venir de diferentes partes de la memoria.

Esta figura muestra un enrejado etiquetado, la Memoria Principal, y partiendo de un par de celdas en el enrejado hay flechas apuntando a la izquierda, a las líneas de cache en una caja. Las líneas se despliegan en una lista, etiquetadas de la 0 a la 3, etcétera.

En los multiprocesadores (computadoras con varias CPUs), los datos escritos deben regresarse a la memoria principal, de forma que el resto de los procesadores puedan verlos, o se debe tener a todos los demás procesadores al tanto de la actividad de la cache local. Tal vez se requiera decirles que invaliden las líneas antiguas que contienen valores previos de la variable escrita, para evitar que accidentalmente usen datos viejos. a esto se le denomina mantener la coherencia entre caches diferentes. El problema se puede volver muy complejo en un sistema multiprocesador. [link] describe la coherencia entre caches con mayor detalle.

Las caches son efectivas porque los programas a menudo exhiben características que ayudan a mantener alta la tasa de aciertos. Estas características se llaman localidad de referencia espacial y temporal ; los programas frecuentemente hacen uso de datos e instrucciones que están cerca de otros datos e instrucciones, tanto en el espacio como en el tiempo. Cuando se carga una línea de cache de la memoria principal, no sólo contiene la información que causó el fallo de la cache, sino también algo de su información circundante. Hay buenas posibilidades de que la próxima vez que su programa necesite datos, éstos se encuentren en la línea de cache recién cargada o en alguna otra reciente.

Las caches trabajan mejor cuando un programa lee secuencialmente a través de la memoria. Asumamos que un programa está leyendo enteros de 32 bits con un tamaño de línea de cache de 256 bits. Cuando el programa hace referencia a la primera palabra en la línea de cache, debe esperar mientras dicha línea se carga desde la memoria principal. Las siete referencias a la memoria subsecuentes se satisfacerán rápidamente desde la cache. Esto se llama paso unitario porque la dirección de cada elemento de datos sucesivo se incrementa en uno, y se usan todos la datos cargados en la cache. El siguiente ciclo funciona con un paso unitario:


DO I=1,1000000 SUM = SUM + A(I)END DO

Cuando un programa accede a una estructura de datos grande usando "pasos no unitarios", el rendimiento sufre porque se cargan datos en cache que no se usan. Por ejemplo:


DO I=1,1000000, 8 SUM = SUM + A(I)END DO

Este código carga la misma cantidad de datos y experimenta el mismo número de fallas en cache que el ciclo previo. Sin embargo, el programa necesita sólo una de las ocho palabras de 32 bits cargadas en la cache. Incluso aunque este programa realiza 1/8 de las sumas que el ciclo anterior, el tiempo que dilata en ejecutarse es aproximadamente el mismo que el otro, porque las operaciones de memoria dominan el rendimiento.

Aunque este ejemplo puede parecer un poco artificial, hay muchas situaciones en las cuales ocurren frecuentemente pasos no unitarios. Primero, cuando se carga en FORTRAN un arreglo bidimensional en memoria, los elementos sucesivos en la primera columna se almacenan secuencialmente, seguidos por los elementos de la segunda columna. Si el arreglo se procesa colocando la iteración de los renglones en el ciclo más interno, produce un patrón de referencias de pasos unitarios como el siguiente:


REAL*4 A(200,200) DO J = 1,200DO I = 1,200 SUM = SUM + A(I,J)END DO END DO

Resulta interesante señalar que muy probablemente un programador en FORTRAN escribirá el ciclo (en orden alfabético) como sigue, produciendo un incremento no unitario de 800 bytes entre operaciones de carga sucesiva:


REAL*4 A(200,200) DO I = 1,200DO J = 1,200 SUM = SUM + A(I,J)END DO END DO

Por esta razón, algunos compiladores pueden detectar este orden de ciclos subóptimo e invertirán el orden de los ciclos para lograr un mejor uso del sistema de memoria. Sin embargo, como veremos en [link] , esta transformación de código puede producir resultados diferentes, y así usted deberá darle "permiso" al compilador para intercambiar esos ciclos en este ejemplo particular (o, tras haber leído este libro, simplemente haberlo codificado apropiadamente desde el comienzo).

while ( ptr != NULL ) ptr = ptr->next;

El siguiente elemento que se recuerda se basa en el contenido del elemento actual. Este tipo de ciclo salta por toda la memoria sin un patrón particular. Se le conoce como caza de apuntadores , y no existe una forma acertada de mejorar el rendimiento de este código.

Un tercer patrón que se encuentra a menudo en cierto tipo de códigos se conoce como acopio (o dispersión), y ocurre en ciclos como:

SUM = SUM + ARR ( IND(I) )

donde el arreglo IND contiene desplazamientos dentro del arreglo ARR. De nuevo, tal como sucedió en la lista ligada, el patrón exacto de referencias a memoria sólo se conoce a tiempo de ejecución, cuando también se conocen los valores almacenados en el arreglo IND. Algunos sistemas de propósito especial tienen soporte de hardware especializado para acelerar esta operación en particular.

Get Jobilize Job Search Mobile App in your pocket Now!

Get it on Google Play Download on the App Store Now




Source:  OpenStax, Cómputo de alto rendimiento. OpenStax CNX. Sep 02, 2011 Download for free at http://cnx.org/content/col11356/1.2
Google Play and the Google Play logo are trademarks of Google Inc.

Notification Switch

Would you like to follow the 'Cómputo de alto rendimiento' conversation and receive update notifications?

Ask