Práctica 2 Rendimiento de memoria caché

Anuncio
Universidad de Alcalá
Departamento de Automática
Arquitectura de Computadores 4º Curso – I. de Telecomunicación
Práctica 2
Rendimiento de memoria caché
1 Objetivos
•
Estudiar el rendimiento y comprender la influencia del tamaño y morfología
en las cachés de las CPUs modernas.
•
Aprender la importancia de la elección de las opciones de compilación de
cara al rendimiento.
•
Estudiar la influencia de los detalles de la arquitectura en la programación
adecuada de un algoritmo.
2 Estudio de rendimiento de la memoria caché
2.1
Introducción
Tal como se ha explicado en teoría, la memoria caché es una memoria relativamente
pequeña, más rápida que la memoria RAM principal, que se utiliza para almacenar
un subconjunto de código o datos que, en base a los principios de localidad espacial
y/o temporal, se espera que se utilicen en un futuro próximo. Lo que se pretende
con esto es que la gran mayoría de accesos a memoria se produzcan sobre la
caché, acelerando el funcionamiento global del programa.
Inicialmente las cachés se encontraban fuera de la CPU y tenían tamaños reducidos
(en torno a 32 o 64 Kbytes); pero la necesidad de mayor velocidad y capacidad, y la
posibilidad tecnológica de integrar la caché dentro de la CPU ha derivado en
sistemas con cachés cada vez más grandes y varios niveles de caché internos y/o
externos a la CPU. Una CPU actual de la familia Intel x86 puede tener hasta 3
niveles de caché de tamaños desde pocos Kbytes hasta varios Mbytes. De hecho
en la actualidad existen CPUs con diferentes denominaciones entre las cuales la
única diferencia es el tamaño de las cachés (por ejemplo, Celeron y Pentium 4 o
Celeron M y Pentium M).
En esta práctica se medirá y valorará el efecto de la memoria caché sobre el
rendimiento de los programas y se dará algún ejemplo de cómo se puede
aprovechar mejor.
2.2
Niveles de caché
Independientemente del fabricante, en una CPU actual de la familia Intel x86
encontramos típicamente un subsistema de caché compuesto por dos niveles,
denominados caché L1 y caché L2.
La caché L1 está integrada en la CPU y funciona a su misma frecuencia de reloj,
suele tener un ancho de bus hacia la CPU mayor y una latencia menor, por lo que
su rendimiento es mayor que el de la caché L2; pero también el coste de integración
es mayor, por lo que su tamaño suele ser pequeño (típicamente entre 8 y
128Kbytes). Además, en algunas arquitecturas la caché L1 se divide en una parte
para instrucciones y otra para datos.
La caché L2 anteriormente solía estar fuera de la CPU y funcionaba a frecuencias
menores que esta, pero actualmente se encuentra casi siempre integrada al igual
que la caché L1. Sin embargo por cuestiones arquitecturales su rendimiento es
menor que la caché L1; aunque también lo es su coste, por lo que normalmente se
encuentra en mayores cantidades. Típicamente su tamaño se encuentra entre los
256Kbytes y los 2Mbytes y no suele haber distinción de caché para programas y
para datos.
El rendimiento de un sistema dependerá por lo tanto de la cantidad de su código y
sus datos que se puedan alojar tanto en la caché L1 como en la caché L2. Si estos
encajan en la caché L1, el programa tendrá el mejor rendimiento posible; si encajan
en las cachés L1 y L2, el rendimiento será menor pero todavía elevado; y si el
programa emplea una cantidad de datos e instrucciones mucho mayores que las
cachés (o se ha programado de forma descuidada), la mejora que se obtendrá del
empleo de cachés será reducida. La elección del tamaño de las cachés depende de
lo que el fabricante considere óptimo y en muchas ocasiones es producto de un
estudio empírico, por lo que diferentes combinaciones de tamaño obtendrán mejor o
peor resultado según la aplicación que se ejecute.
Obviaremos en esta práctica las implicaciones derivadas de política de asociatividad
de la caché o del tamaño de las líneas de caché.
2.3 Medidas de ancho de banda
En la primera parte de la práctica se medirá la velocidad de transferencia entre CPU
y memoria, tanto en lectura (de memoria a CPU) como en escritura (de CPU a
memoria). Para ello, se leerá y escribirá un mismo bloque de datos en memoria de
tamaño fijo un cierto número de veces, y se medirá el tiempo empleado en la
operación. Con esta información se podrá calcular la velocidad de transferencia en
megabytes por segundo (Mb/s) a la que se puede realizar dicha operación. Por
ejemplo, si transferimos un bloque de 128Kbytes 1024 veces y en ello tardamos 2
segundos, la velocidad de transferencia resultante es de aproximadamente 64Mb/s.
Dado que la medida se realizará repitiendo la transferencia de los mismos datos una
y otra vez, es de esperar que las cachés influyan en los resultados en función del
tamaño del bloque empleado. La primera vez que el bloque se lea o escriba se
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 2
producirán fallos de caché que provocarán la carga de las líneas correspondientes,
pero las repeticiones sucesivas en el acceso al bloque ya no provocarán fallo salvo
que las líneas de caché hayan tenido que ser sustituidas por otras (por ejemplo si el
bloque es más grande que la caché).
Efectivamente, si por ejemplo el bloque transferido es lo suficientemente pequeño
para estar contenido íntegramente en la caché L1, las sucesivas lecturas o
escrituras se realizarán sobre ésta, con lo que se medirá la velocidad de
transferencia efectiva entre la CPU y la caché L1. Si el bloque es demasiado grande
para la caché L1 pero puede estar contenido en la caché L2, mediremos la
velocidad de transferencia entre la CPU y la caché L2 (con cierta influencia de la
caché L1, evidentemente). Y finalmente, si el bloque de datos es demasiado grande
para alojarse en las cachés L1 y L2, habrá líneas de memoria que al cargarlas en
caché sustituirán a otras del mismo bloque, con lo que el uso de la caché no
ayudará demasiado y estaremos midiendo prácticamente la velocidad de
transferencia entre la CPU y la memoria RAM.
2.4 Mejoras en el rendimiento mediante técnicas de
programación
En muchas ocasiones, en el rendimiento de una función o un programa influye
haber realizado una programación cuidadosa que tenga en cuenta detalles
arquitecturales. En esta prácticas se estudiará el hecho de que organizar los datos
que se van a utilizar de forma consecutiva en memoria puede ayudar a mejorar el
rendimiento dado que la caché se gestiona en base a líneas de memoria
compuestas por posiciones de memoria consecutivas. Así, cuando se accede a un
dato la CPU carga en la caché toda una línea de memoria en la que éste se
encuentra, y si los datos que vamos a emplear a continuación se encuentran en
dicha línea podremos acceder a ellos sin provocar ningún fallo más de caché.
Para ilustrar este hecho se empleará como ejemplo un programa de multiplicación
de matrices.
En un programa cualquiera, una estructura de datos de tipo matriz puede tener más
de una dimensión; pero debe almacenarse en la memoria, que es básicamente un
espacio de almacenamiento lineal, no multidimensional; es decir, la memoria tiene
un único índice a través del cual la accedemos en realidad (la dirección de memoria
del dato accedido). En C, una matriz bidimensional queda almacenada colocando
las filas de la misma una detrás de otra en memoria, y es la lógica del compilador la
que nos permite movernos por filas y columnas como si el espacio de memoria fuera
bidimensional empleando múltiples subíndices para denotar fila y columna.
La consecuencia de esto es que cuando se visita una matriz accediendo
sucesivamente a los elementos de una misma fila (p.e. matriz[1][1], matriz[1][2],
matriz[1][3], etc.) se accede a posiciones consecutivas de memoria, mientras que
cuando lo hacemos accediendo a elementos de una misma columna (p.e.
matriz[1][1], matriz[2][1], matriz[3][1], etc.) se accede a posiciones no consecutivas
en la memoria. El siguiente diagrama representa la situación descrita con una matriz
de números enteros de 32 bits de 4 columnas.
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 3
Por otro lado recordemos que cuando se quieren multiplicar dos matrices A y B, se
obtiene cada el elemento de la matriz resultado C[i][j] como la suma de los
productos de cada elemento de la fila i de la matriz A por cada elemento de la
columna j de la matriz B. Por lo tanto el algoritmo típico visita los elementos de la
matriz A recorriendo las filas, y los elementos de la matriz B recorriendo las
columnas. Como consecuencia de lo explicado en el párrafo anterior, esto implica
que cuando se accede a los elementos de la matriz A se accede a la memoria de
forma consecutiva (y por lo tanto a datos en la misma línea de caché), pero cuando
se accede a la matriz B se accede a posiciones salteadas de memoria, lo que
potencialmente puede provocar accesos a diferentes líneas de caché y lleva a una
tasa de fallos de caché mayor.
Para obtener un mejor rendimiento, es posible realizar una modificación en la
realización del algoritmo que consiste en lo siguiente: se transpone la matriz B (de
forma que las filas se transforman en columnas y viceversa), y obtenemos C[i][j]
como la suma de los productos de cada elemento de la fila i de la matriz A por cada
elemento de la fila j de la matriz B. Dado que hemos transpuesto la matriz B el
resultado de la multiplicación es exactamente el mismo, pero las diferencias de
velocidad son considerables, como comprobará el alumno, dado que tanto al
acceder a la matriz A como a la matriz B lo hacemos recorriendo sus filas.
2.5
Influencia de las opciones de compilación
Finalmente, otro factor determinante en el rendimiento de un programa es emplear
las opciones de compilación adecuadas a la arquitectura en lugar de dejar las
opciones por defecto que, por lo general, realizan una compilación para el máximo
común denominador (es decir, en el caso de Intel, el 80386).
En casos como los que nos ocupan en los que se realizan medidas de rendimiento
del ancho de banda de memoria y la influencia de las cachés, lo típico es elaborar
los programas en lenguaje ensamblador ya que el compilador de C puede introducir
código adicional o formas de tratar los datos que falseen los resultados. Dado que
las prácticas se realizarán de todas formas en lenguaje C, es fundamental indicar al
compilador ciertas directrices que le obliguen a generar código lo más óptimo
posible para la CPU concreta y que por lo tanto dé resultados lo más aproximados al
caso ideal posibles.
El uso de estas opciones es muy importante, pero lo es mucho más en esta práctica.
El alumno debe experimentar con ellas de forma que pueda valorar su importancia,
comparando los rendimientos obtenidos al compilar y ejecutar el mismo programa
con diferentes opciones.
Algunas opciones importantes que tendrán influencia en los programas a realizar en
esta práctica son las siguientes (pueden consultarse en la página del manual
dedicada al compilador gcc).
•
-On
Optimización, desde n=0 (ninguna) a n=3 (máxima). Por defecto, el compilador no
optimiza nada. Emplear más optimización requiere mayor tiempo de compilación,
pero genera código en principio más rápido y eficiente. En general un nivel 1 de
optimización obtiene un rendimiento suficiente sin emplear demasiado tiempo.
•
-march=arquitectura
Genera código específico para la arquitectura indicada. Esto implica usar
instrucciones máquina específicas, juegos de instrucciones avanzados como MMX o
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 4
SSE y tener en cuenta ciertas peculiaridades de la arquitectura. Algunos ejemplos
de arquitecturas soportadas son pentium-m, pentium4, athlon, athlon64, etc.
La lista completa puede consultarse en la página del manual indicada.
•
-mfpmath=sse
Utilizar instrucciones extendidas SSE (Streaming SIMD Instructions) para las
operaciones de coma flotante si la CPU las soporta, que es el caso de casi cualquier
CPU moderna. Esto conlleva un mayor rendimiento no sólo en las operaciones en
sí sino en las transferencias CPU-memoria de datos de coma flotante, ya que es
posible transferir con una sola instrucción datos de mayor tamaño, como long
double.
•
-m128bit-long-double
Emplear 128 bits para los datos de tipo long double, en lugar de los 96 bits que
indica el estándar. Esto permite que los datos de este tipo estén alineados en
memoria a 16 bytes, lo que ayuda a mejorar el rendimiento en las transferencias
CPU-memoria cuando se trabaja con ellos.
3 Influencia de la planificación en la medida del
rendimiento
3.1 Cuestiones de planificación
Si se desea medir el tiempo que tarda en realizarse cualquier tarea, es necesario
evitar que pueda producirse cualquier tipo de interferencia mientras se hace la
medida. En un sistema multiprogramado esto es prácticamente imposible dado que
no sólo se estarán atendiendo interrupciones constantemente, sino que el
planificador puede requisar la CPU mientras realizamos la medida y poner a ejecutar
cualquier otro proceso, restando precisión o falseando los resultados.
En el caso que nos ocupa no podemos hacer nada respecto a las interrupciones
dado que la única solución sería deshabilitarlas y eso no es factible para un
programa de usuario. Pero sí podemos influir en la planificación de forma que
evitemos que el programa que realiza las medidas se vea interrumpido por otros.
Para ello, en el caso de Linux, podemos emplear determinadas llamadas que
indican al sistema operativo la política de planificación que deseamos que se
emplee para el proceso, y la prioridad que se le debe asignar. Asignando la máxima
prioridad y un tipo de planificación de “casi tiempo real” como por ejemplo
SCHED_FIFO, evitaremos la intrusión del planificador en las mediciones.
3.2 Algunas llamadas al sistema para gestión de la
planificación
3.2.1
Introducción
En Linux existen tres tipos de políticas de planificación: Por una parte tenemos la
política SCHED_OTHER, que es la que se asigna a todos los procesos normales del
sistema, y por otra tenemos las políticas SCHED_FIFO y SCHED_RR, que son
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 5
políticas para procesos que sean críticos en el tiempo de respuesta y de mayor
prioridad, y que por lo tanto requieren un control más fino sobre la forma en que se
realice la planificación. A veces se encuentran referencias a estas políticas de
planificación como “de tiempo real”, pero debe quedar claro que no proveen de
tiempo real de verdad si no en todo caso una aproximación.
En la práctica, y especialmente en el caso que nos ocupa, cualquier proceso
SCHED_FIFO o SCHED_RR se ejecutará antes que cualquier otro proceso
SCHED_OTHER, por lo que si un sistema sólo tiene procesos SCHED_OTHER (que es
lo habitual), un proceso SCHED_FIFO o SCHED_RR se ejecutará siempre que quiera
sin interrupción.
Es por esto que sólo el superusuario (root) puede realizar llamadas al sistema que
establezcan este tipo de planificación, y hay que ser muy cuidadoso con los
programas que la emplean puesto que un bucle infinito en un programa de estas
características provocaría un “cuelgue” general de todo el sistema ya que ningún
otro proceso tendría oportunidad de ejecutarse (ni siquiera los procesos del
sistema).
Para las prácticas, se establecerá la política SCHED_FIFO con la máxima prioridad
con el fin de obtener los resultados más fiables posibles. Sin embargo el alumno
deberá incorporar este control de la planificación a la práctica sólo cuando ésta
funcione sin problemas, para lo cual deberá lanzar el programa como root. En el
caso del laboratorio no podrá lanzarse el programa como root por lo que los
resultados serán un poco menos precisos pero servirán para probar el
funcionamiento. Tenga también en cuenta que cuando se aumenta la prioridad de un
proceso de esta forma, el sistema aparentemente se congela dado que no se
atienden los procesos que hacen de interfaz de usuario (el sistema de ventanas o la
consola). Por ello es también conveniente realizar primero las pruebas sin modificar
la prioridad y comprobar que los programas se ejecutan en un tiempo razonable y
sin problemas.
3.2.2
sched_get_priority_max
La declaración de sched_get_priority_max es:
int sched_get_priority_max(int politica);
donde:
politica
Política de la cual se quiere obtener la máxima prioridad (p.e.
SCHED_FIFO).
La llamada devuelve un entero correspondiente a la máxima prioridad numérica del
tipo de política especificado o bien -1 en caso de error.
3.2.3
sched_setscheduler
Esta llamada se emplea para establecer la política de planificación y la prioridad de
un proceso. Su declaración es:
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 6
int sched_setscheduler(pid_t pid, int politica, const struct
sched_param *p);
donde:
pid
PID del proceso sobre el que actuar, o 0 para el proceso que
realiza la llamada.
politica
Política a establecer (p.e. SCHED_FIFO).
p
Otros parámetros de planificación, como la prioridad.
La llamada devuelve un entero diferente de 0 en caso de éxito o 0 en caso de error.
La estructura sched_param puede consultarse en la página del manual y tiene la
siguiente forma:
struct sched_param {
...
int sched_priority; // Prioridad a establecer
...
};
4 Tareas a realizar
4.1
Introducción y cómo presentar los resultados
Esta práctica está dividida en dos ejercicios, tal como se ha descrito en la sección 2.
El primer ejercicio versará sobre la medida del ancho de banda de memoria en
función del tamaño del bloque de datos transferidos, y el segundo sobre la mejora
en el rendimiento al emplear una técnica alternativa para multiplicar dos matrices
que tiene en cuenta las características de las memorias caché.
En ambos casos se exige al alumno que los resultados se almacenen en un archivo
de texto de forma que pueda realizarse a posteriori una representación gráfica de
los mismos con un programa como Matlab o GNUplot. Si el alumno decide mostrar
información por pantalla será a título informativo, pero no se aceptarán estos
resultados como resultados del programa.
En caso de emplear GNUplot, una herramienta gratuita de generación de gráficos,
la forma de generar los resultados para esta práctica es muy sencilla y se describe a
continuación.
El archivo de datos que tomará como entrada GNUplot debe ser de texto plano, con
la información ordenada en filas y columnas. Las columnas están separadas por
espacios en blanco o tabuladores, y las filas están delimitadas por retornos de carro.
Por ejemplo podríamos tener un archivo parte1.dat con el contenido:
1024
6262.49 5916.32
2048
6127.54 6026.30
3072
6222.91 6063.23
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 7
4096
6213.21 6152.26
5120
6290.57 6144.28
6144
6314.55 6146.25
...
Este archivo corresponde a una realización de uno de los ejercicios, y en él la
primera columna indica el tamaño del bloque utilizado para las transferencias (en
bytes), la segunda la velocidad de transferencia en Mb/s para lectura y la tercera la
velocidad de transferencia en Mb/s para escritura. En principio el formato del archivo
es libre, y el alumno puede indicar los datos que quiera en el orden que quiera y el
formato numérico que quiera, dado que GNUplot simplemente los trata como
números para representar. Para generar el archivo se recomienda al alumno que
emplee funciones como fprintf(), o bien sprintf()combinado con write().
Si a continuación quisieramos que GNUplot representase los datos del ejemplo
anterior, lo invocaríamos desde la línea de órdenes (con el comando gnuplot) y
una vez cargado el programa indicaremos lo siguiente:
gnuplot> plot "parte1.dat" using 1:2 with lines,
"parte1.dat" using 1:3 with lines
Sin entrar en muchos detalles, este comando indica que se representen dos gráficas
(que se especifican separadas por la coma): La primera, tomando como origen de
datos el archivo “parte1.dat” y representando en los ejes X e Y la columna 1 contra
la 2, uniendo los puntos con líneas. Y la segunda, tomando como origen el mismo
archivo y representando en los ejes X e Y la columna 1 contra la 3, uniendo también
los puntos con líneas. El gráfico resultante es el mostrado a continuación (que no
necesariamente representa un resultado correcto para el problema planteado):
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 8
En el gráfico tenemos, en el eje X el tamaño de bloque (columna 1 del archivo), y en
el eje Y las velocidades de transferencia en Mb/s (columnas 2 y 3).
Como se puede observar la gráfica no tiene etiquetas para los ejes y está escalada
en función de los datos que GNUplot encuentra en el archivo, dado que se le han
especificado las opciones mínimas al comando plot. Se deja como ejercicio para el
alumno investigar un poco más el funcionamiento del programa, así como las
opciones para generar gráficos más atractivos. Se recomienda consultar la
documentación de GNUplot o cualquiera de los muchos tutoriales existentes en la
red.
4.2
Ejercicio 1: Medidas de ancho de banda de memoria
Este ejercicio realizará medidas de velocidad de transferencia de CPU a memoria y
de memoria a CPU transfiriendo repetidas veces cada elemento de un array de
datos a un registro de la CPU, y viceversa.
Como ejemplo, el código que realiza la lectura de memoria a CPU podría tener una
forma similar a esta:
for( n = 0; n < num_repeticiones; n++)
for( i = 0; i < num_elementos_array; i++)
acc = array[i];
En este ejemplo se lee num_repeticiones veces el array completo, que tiene
num_elementos. Por tanto los bytes transferidos en total serán
num_repeticiones por num_elementos por el tamaño de cada elemento en
bytes. Si medimos el tiempo empleado en la operación tal como se hizo en la
práctica 1 (con llamadas a gettimeofday()), tendremos información suficiente
para calcular la velocidad de transferencia en Mb/s.
Observe que en el ejemplo acc es una variable (y por lo tanto una posición de
memoria) y no un registro de la CPU; pero confiaremos en las optimizaciones del
compilador para que emplée un registro para almacenar acc.
El programa a realizar irá repitiendo la prueba anterior variando el tamaño del
bloque desde 1Kb hasta 4Mbytes en pasos que el alumno juzgue adecuados pero
que permitan una representación precisa, y almacenará en un archivo, tal como
explica el apartado anterior, una tabla donde se indique en la primera columna el
tamaño del bloque empleado y en las siguientes las velocidades medidas en lectura
y escritura, de forma que pueda hacerse una representación gráfica de tamaño de
bloque vs. velocidad de transferencia.
Tenga en cuenta que la zona de interés es sobre todo aquella que es representativa
para el tamaño de las cachés, por lo que puede ser interesante variar el tamaño de
bloque en pequeños pasos al principio (por ejemplo incrementos de 1Kb) y en pasos
más grandes después para reducir el tiempo total de ejecución manteniendo la
precisión en la parte izquierda de la gráfica que es la más importante.
Para obtener un resultado lo más fiable posible, el array de datos a transferir será de
datos de tipo long double puesto que son los que los más grandes que los
procesadores modernos pueden mover en un único ciclo de reloj. Se reservará
espacio inicialmente para el tamaño máximo de bloque que se vaya a usar (por
ejemplo, 4Mbytes) y en lo sucesivo se irán usando subconjuntos de este bloque
para realizar las medidas. Es indiferente si se realiza una reserva estática o
dinámica del bloque, pero es fundamental realizar una escritura completa de sus
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 9
contenidos con unos datos cualquiera (por ejemplo ceros) al iniciar el programa para
que se provoquen los fallos de página pertinentes antes de comenzar las pruebas y
no durante las mismas, lo cual falsearía los resultados.
Tenga en cuenta también que para que los resultados sean fiables deben realizarse
suficientes repeticiones como para que el sistema tarde un tiempo apreciable en
realizar las operaciones, dado que la precisión en la medida del tiempo en un PC es
de milisegundos; un procesador moderno es capaz de transferir datos a velocidades
de varios Gbytes/s, por lo que pocas repeticiones finalizarían en pocos milisegundos
y la precisión sería muy reducida.
Por otro lado, realizar un número elevado de repeticiones fijo puede emplear un
tiempo razonable para tamaños de bloque pequeños, pero sería problemático para
tamaños de bloque mayores. Se recomienda por lo tanto decidir una cantidad fija de
datos totales a transferir en cada prueba (por ejemplo 1Gbyte) y calcular el número
de repeticiones en función del tamaño del bloque que se desea probar. Por ejemplo,
si se elige 1Gbyte como cantidad de referencia se realizarían aproximadamente
1.000.000 repeticiones para un tamaño de bloque de 1Kb, y unas 1.000 repeticiones
para un tamaño de bloque de 1Mb. Dado que el tiempo de prueba total puede ser
largo (de varios minutos), es recomendable empezar haciendo pruebas con
tamaños de referencia pequeños y pasos de tamaño de bloque grandes. Como dato
orientativo, es conveniente que para cada tamaño de bloque se tarde alrededor de 1
o 2 segundos en realizar la prueba.
Se recomienda también realizar inicialmente la práctica mostrando los resultados
por pantalla y sin realizar modificaciones en la política de planificación, y cuando
todo funcione incluir estos aspectos en la práctica.
Como referencia, la figura representada a continuación corresponde a las medidas
de lectura de memoria en dos sistemas de prueba:
- AMD Athlon 64 3000+ (1.8Ghz) cuyas características son: Caché L1 de 128Kb,
caché L2 de 512Kb, memoria RAM de tipo DDR400 (velocidad de pico teórica de
3.2Gb/s)
- Pentium-M 1.6Ghz cuyas características son: Caché L1 de datos de 32Kb, caché
L2 de 2Mb, memoria RAM de tipo DDR333 (velocidad de pico teórica de 2.7Gb/s)
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 10
En el eje X se representa el tamaño del bloque transferido y en el Y la velocidad en
Mb/s. Se observan claramente en la figura tres zonas diferenciadas en cada caso.
La primera corresponde a tamaños de bloque que caben en la caché L1. Se observa
que en el caso del Athlon64 esta zona es más ancha por su caché L1 de 128Kb
frente a la de 32Kb del Pentium-M. En segundo lugar se encuentra la zona en que el
bloque de prueba cabe en la caché L2. En el caso del Athlon 64 esta zona sólo
abarca hasta bloques de 512Kb aproximadamente, mientras que el Pentium-M
mantiene el rendimiento hasta alcanzar los 2Mb, donde el bloque es mayor que la
caché L2 y entonces la limitación la impone el ancho de banda de la memoria RAM.
Al ser ésta más rápida en el Athlon64, el rendimiento para bloques grandes es
mayor en esta CPU.
Los resultados obtenidos por el alumno deben mostrar valores similares y sobre
todo unas zonas diferenciadas, todo ello dependiente de la velocidad y de las
características de la caché de la CPU concreta así como de la memoria RAM. Para
ello, puede ser interesante obtener la información de la CPU de la documentación
disponible en Internet y de la obtenida por el comando “cat /proc/cpuinfo”:
processor
vendor_id
cpu family
model
model name
stepping
cpu MHz
cache size
...
:
:
:
:
:
:
:
:
0
GenuineIntel
6
13
Intel(R) Pentium(R) M processor 1.60GHz
6
600.000
2048 KB
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 11
4.3
Ejercicio 2: Métodos de multiplicación de matrices.
Este ejercicio demostrará las mejoras sustanciales que se pueden obtener en las
operaciones de multiplicación de matrices en virtud de lo expuesto en el apartado
2.4.
Para ello, y para simplificar el ejercicio, el programa multiplicará dos matrices
cuadradas de elementos de tipo long double mediante los dos métodos
expuestos, midiendo los tiempos de multiplicación en ambos casos. La medida se
repetirá para matrices desde dimensiones muy reducidas (2x2) hasta dimensiones
que desborden las cachés de la CPU (por ejemplo, 400x400).
Al igual que en el caso anterior, las matrices a multiplicar y la matriz de resultado se
reservarán una única vez al principio del programa con el tamaño máximo que se
vaya a usar y se inicializarán con unos valores cualesquiera pero que provoquen los
fallos de página pertinentes, y a continuación se usarán subconjuntos de estas
matrices para la realización de las pruebas. Es indiferente que las matrices se
reserven de forma estática o dinámica (se recomienda que se reserven de forma
estática en esta ocasión).
También para simplificar, la matriz B no es necesario trasponerla para realizar las
pruebas sino que se supondrá ya transpuesta. Al ser la matriz B cuadrada, las
dimensiones de la matriz y su transpuesta son iguales; y el contenido de las
matrices y el resultado de la multiplicación nos es indiferente para nuestros
propósitos. Lo único que debe preocuparnos al realizar el programa es la medida del
tiempo de multiplicación suponiendo que la matriz B está sin transponer (método
clásico) y suponiendo que está transpuesta (método expuesto en el apartado 2.4).
El programa almacenará los resultados en un archivo de texto similar al del apartado
anterior, en el que la primera columna indicará las dimensiones de las matrices
multiplicadas, la segunda y tercera los tiempos empleados en las multiplicaciones
empleando ambos algoritmos, y la cuarta la ganancia de rendimiento (es decir, el
tiempo empleado con el algoritmo tradicional partido por el tiempo empleado con el
algoritmo mejorado).
Como referencia, a continuación se muestran los resultados obtenidos en un
Pentium-M 1.6Ghz de características iguales a las del apartado anterior. La primera
gráfica muestra el tiempo empleado en cada caso frente a la dimensión de las
matrices, y el segundo la ganancia también frente a la dimensión de las matrices
(una ganancia de 1 significa que no se gana tiempo).
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 12
Se puede observar que mientras las matrices a multiplicar caben en la caché, la
ganancia es nula (es decir, cercana a 1), mientras que en cuanto las matrices
superan el tamaño de la caché el nuevo método obtiene ganancias de tiempo
sustanciales, mayores cuanto mayores son las matrices, debido a la reducción en
los fallos de caché.
5 Cuestiones
•
Genere los programas empleando diferentes opciones de compilación de
entre las expuestas en la sección 2 y compare el rendimiento.
•
Analice la diferencia entre modificar la política de planificación a
SCHED_FIFO y elevar la prioridad, y no hacerlo. ¿Varía sustancialmente el
rendimiento?¿Y la estabilidad en los resultados?
Arquitectura de Computadores, 4º I. Telecomunicación – Práctica 2 – Página 13
Descargar