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