Full text
Proyecto fin de carrera Ingeniero Técnico en Informática de Sistemas MODIFICACIÓN DEL PLANIFICADOR DE LINUX PARA EXTRAER INFORMACIÓN DE RENDIMIENTO DE LOS PROCESOS Jorge Pastor Pérez Dirigido por Vicent Lorente Garcés Departamento de Informática de Sistemas y Computadores Escuela Técnica Superior de Ingeniería Informática Universidad Politécnica de Valencia Valencia, septiembre de 2011
2
Índice de contenido Resumen...............................................................................................................................................4 1 Introducción.......................................................................................................................................5 2 Estado del arte...................................................................................................................................7 2.1 Scheduling Algorithms with Bus Bandwidth Considerations for SMPs...................................7 2.2 On Mitigating Memory Bandwidth Contention through Bandwidth-Aware Scheduling........12 3 Trabajo relacionado.........................................................................................................................16 3.1 Arquitectura de un procesador.................................................................................................16 3.1.1 Introducción a la microarquitectura Core........................................................................16 3.1.2 La jerarquía de memoria..................................................................................................16 3.1.3 La unidad de monitorización del rendimiento.................................................................18 3.2 Los contadores de rendimiento en el sistema operativo..........................................................22 3.2.1 La llamada al sistema sys_perf_event_open().................................................................22 3.3 Benchmarks.............................................................................................................................28 3.3.1 SPEC CPU2006...............................................................................................................28 4 Experimentos y datos......................................................................................................................32 4.1 Desarrollo de una herramienta para la toma de datos..............................................................32 4.1.1 Diseño..............................................................................................................................32 4.1.2 Lanzamiento de las aplicaciones......................................................................................33 4.1.3 Recogida de datos de los benchmarks..............................................................................33 4.1.4 Selección y conversión de los datos.................................................................................38 4.1.5 Representación de la información....................................................................................40 4.2 Realización de las pruebas de rendimiento..............................................................................42 4.2.1 Configuración del entorno de ejecución..........................................................................42 4.2.2 Resultados de la ejecución de los benchmarks................................................................42 5 Conclusiones y trabajo futuro..........................................................................................................57 Bibliografía.........................................................................................................................................58 3
Resumen En los últimos tiempos, los sistemas multiprocesador han proliferado. En los sistemas multiprocesador, diferentes núcleos del procesador compiten por el acceso al bus de memoria cuando deben acceder a datos que no están disponibles en memoria caché, debido a que el acceso a memoria principal se realiza en serie. Esto es un importante cuello de botella arquitectural que puede hacer que el rendimiento se vea afectado. El planificador de procesos es un elemento esencial en los sistemas operativos multiprogramados, el cual decide qué proceso debe pasar a CPU de entre los que se encuentran preparados para ejecución. En los planificadores de tiempo compartido actuales, se planifica por prioridad y afinidad de los procesos. La afinidad de los procesos trata de evitar dentro de lo posible las migraciones de procesos entre unidades de ejecución de la CPU para mantener el estado que el proceso tiene en la caché y no tener que afrontar el coste que supone generarlo. Sin embargo, los planificadores actuales no abordan directamente las limitaciones del ancho de banda del bus. Como puede verse los estudios anteriores [1] y [2], planificar teniendo en cuenta el ancho de banda del bus de la memoria puede significar una mejora en el rendimiento de los sistemas multiprocesador. En los procesadores modernos, existen contadores de monitorización del rendimiento, que permiten obtener información precisa de cómo se comporta el sistema. Los sistemas operativos actuales permiten tener acceso a estos contadores siempre y cuando sean ofrecidos por la arquitectura hardware. Basándose en estos datos, es posible caracterizar el comportamiento de las aplicaciones a lo largo del tiempo, y guiar a un planificador de procesos de modo que planifique teniendo en cuenta las limitaciones del ancho de banda del bus. Para ello, previamente es necesario comprender el funcionamiento de los contadores y diseñar métodos para recoger los datos, tratarlos e interpretarlos. Todo esto es posible con la ayuda de programas de prueba llamados benchmarks, que se comportan de forma predecible y permiten comparar los resultados entre ejecuciones. 4
1 Introducción Los multiprocesadores de memoria compartida, como los recientes procesadores multinúcleo, son ampliamente utilizados como una vía asequible para construir desde sistemas de alto rendimiento hasta equipos de escritorio o incluso dispositivos móviles. El bus que interconecta a los procesadores con la memoria principal ha llegado a ser el cuello de botella principal que dificulta la escalabilidad de estos sistemas. Debido a esto, en los últimos años se han realizado diversos estudios y se han propuesto algunas políticas de planificación conscientes del ancho de banda del bus de memoria que tratan de mitigar el problema. Los planificadores actuales utilizados en sistemas con multiprocesadores de memoria compartida emplean tiempo compartido con prioridades dinámicas y afinidad de los procesos. La afinidad de los procesos tiene como objetivo intentar planificar siempre en el mismo procesador aquellos procesos que hayan conseguido construir su estado en la caché, para evitar afrontar el coste que supone generarlo. Algunos planificadores utilizan algoritmos de espacio compartido, que dividen los procesadores entre programas, de modo que cada uno de ellos se ejecute en un subconjunto de los procesadores del sistema, cuyo tamaño puede variar en tiempo de ejecución. Gracias a la mejora de la localidad, se tiende a aumentar el rendimiento de la caché en tareas paralelas. Sin embargo, se limita el grado de paralelismo que la aplicación puede aprovechar. Ciertos estudios muestran la importancia del ancho de banda de memoria para el rendimiento de los sistemas con multiprocesadores de memoria compartida, y plantean políticas de planificación alternativas. En las políticas de planificación según el consumo de ancho de banda del bus de memoria, se busca planificar tareas de modo que se realice una utilización del bus de memoria inteligente, que ni lo sature ni lo infrautilice. Para poder guiar a un planificador en sus decisiones dependiendo del consumo de ancho de banda de memoria del bus, hay que predecir cuáles van a ser los requisitos de una aplicación, tarea o hilo. Si se quiere conocer el comportamiento de una aplicación, hay que caracterizarla correctamente. Con el fin de caracterizar las aplicaciones es necesario emplear los contadores de rendimiento disponibles en las CPUs actuales. Estos contadores permiten registrar diferentes tipos de eventos, dependiendo de la microarquitectura del procesador. Los eventos pueden ser utilizados para caracterizar las aplicaciones en cuanto a accesos a memoria o utilización del ancho de banda del bus de la memoria, entre otras cosas. Para poder medir correctamente con los contadores de rendimiento, también es necesario contar con la ayuda de un sistema operativo debidamente capacitado. Un planificador orientado al consumo de memoria podrá tomar las decisiones en cada cambio de contexto. Para ello, hay dos posibles escenarios, dependiendo de las necesidades. Por un lado, es posible realizar previamente la caracterización de las tareas para guiar posteriormente al planificador. Esto tiene la ventaja de guiar al planificador con el comportamiento preciso de las aplicaciones, pero tiene un uso muy limitado. Por otro lado, existe la posibilidad de guiar al planificador según el comportamiento en tiempo real de las aplicaciones, tomando para ello una ventana de muestras antes del instante actual. En cualquier caso, es necesario medir con los contadores de rendimiento. Para poder evaluar la herramienta de medida implementada en el proyecto, se utilizan benchmarks. Los benchmarks proporcionan un entorno de ejecución predecible, el cual puede reproducirse tantas veces como sea necesario. De este modo, es posible analizar los resultados tras 5
realizar cambios en la herramienta o el entorno de ejecución, permitiendo extraer conclusiones y efectuar ajustes oportunos. El resto del proyecto está organizado como sigue: en el punto 2, se habla de algunos estudios existentes relacionados con la saturación del bus de la memoria y políticas de planificación en sistemas multiprocesador. En el punto 3 se detalla el trabajo relacionado, como la arquitectura de la CPU, el sistema operativo o las pruebas de rendimiento. A continuación se pasa en el punto 4 a explicar los procedimientos empleados y los resultados de las pruebas realizadas. Por último, en el punto 5, se encuentran las conclusiones y el trabajo futuro. 6
2 Estado del arte 2.1 Scheduling Algorithms with Bus Bandwidth Considerations for SMPs En [1], C. D. Antonopoulos et al. observan que en multiproceso simétrico ninguno de los algoritmos de planificación anteriormente propuestos ha sido impulsado por los efectos de compartir otros recursos del sistema que no fueran las cachés y los procesadores. En concreto, ninguna de las políticas está impulsada por el impacto de la compartición del bus, o en general, de la red que interconecta los procesadores y la memoria. Además, de entre todas las políticas que se centran en optimizar el rendimiento de la memoria, ninguna considera el ancho de banda disponible entre los diferentes niveles de la jerarquía de memoria como un factor para orientar las decisiones de planificación. Tras esta reflexión, los autores efectúan un estudio de las implicaciones que tiene el ancho de banda del bus en el rendimiento de las aplicaciones. Para realizar las experiencias, se utilizan aplicaciones optimizadas de dos benchmarks, NAS y Splash-2. La plataforma experimental consiste en un sistema con 4 procesadores simétricos, cada uno de los cuales tiene su propia memoria caché de segundo nivel. El sistema operativo es un Linux de la rama 2.4. La tasa de transferencia máxima sostenida en el bus se estima en 29'5 transacciones/μs. Las pruebas se dividen en cuatro grupos. El primero mide el ancho de banda consumido por cada aplicación, cuando se ejecuta en solitario utilizando dos procesadores. En el segundo grupo, se ejecutan dos instancias idénticas de una aplicación, utilizando dos procesadores cada una. En el tercer grupo, se ejecuta una instancia de la aplicación que utiliza dos procesadores, junto a dos instancias de un microbenchmark BBMA. El microbenchmark BBMA está diseñado de modo que obtenga una tasa de acierto cercana al 0% y realice constantemente accesos a memoria, consumiendo una fracción significativa del ancho de banda del bus disponible. El cuarto grupo es idéntico al tercero, excepto por la configuración del microbenchmark. En este caso se utiliza un microbenchmark nBBMA, que está diseñado para obtener una tasa de acierto cercana al 100%, a pesar de los constantes accesos a memoria, generando muy poco tráfico en el bus. En las barras negras de la figura 1 se muestra el ancho de banda del bus consumido por cada aplicación (primer grupo de pruebas). Dado que el ancho de banda máximo consumido es de 23'31 transacciones/μs, en este caso el bus ofrece suficiente ancho de banda para las aplicaciones en solitario. 7
Las barras en gris oscuro representan al segundo grupo. Las aplicaciones con los mayores requisitos de ancho de banda ponen al bus cerca del límite. En la figura 2 se observa la pérdida de rendimiento sufrida por las aplicaciones, debido a la competición por el bus. Los resultados muestran que las aplicaciones con requisitos de ancho de banda altos sufren una mayor degradación. El tercer grupo está representado por las barras gris claro. Estos experimentos muestran el impacto de tener aplicaciones funcionando en un bus ya saturado. El ancho de banda consumido por cada carga de trabajo se encuentra cerca del límite de saturación. Las aplicaciones que realizan accesos intensivos a memoria sufren importantes pérdidas de rendimiento en este escenario. Por último, las barras blancas rayadas se corresponden con el cuarto grupo. Tanto la tasa de transferencia de datos en el bus como el tiempo de ejecución de las aplicaciones son casi idénticos a los del primer grupo. Esto indica que combinar aplicaciones con altos y bajos requisitos de ancho de banda, es un buen método para alcanzar mayores tasas de rendimiento. Los resultados obtenidos les motivan para investigar nuevas políticas de planificación que sean guiadas por el consumo del ancho de banda del bus. En total son implementadas dos nuevas políticas de planificación, las cuales planifican las tareas teniendo en cuenta el ancho de banda que consumen. El objetivo es optimizar el uso del ancho de banda del bus del sistema, planificando tareas que ni infrautilicen ni saturen el bus. La información requerida para guiar a las políticas de 8 Figura 1: Tasa de transferencia del bus acumulada Figura 2: Pérdida de rendimiento
planificación se obtiene de los contadores de monitorización del rendimiento presentes en los procesadores modernos. Estos contadores permiten realizar un análisis de los cuellos de botella que afectan al rendimiento. La información, que es posible obtener de estos contadores, no se había utilizado previamente en tiempo de ejecución para influir en las decisiones de planificación de un sistema real. La primera política de planificación implementada se conoce como Latest Quantum. Al final de cada quantum del planificador, esta política actualiza las estadísticas de consumo de ancho de banda del bus para todas las tareas en ejecución, utilizando la información proporcionada por las aplicaciones. Las aplicaciones que anteriormente habían estado en ejecución, pasan al final de la lista. A continuación, el planificador selecciona las aplicaciones que serán ejecutadas en el siguiente quantum. Para evitar la inanición, se selecciona por defecto la primera aplicación de la lista. Cada vez que se selecciona una aplicación para ejecución, el ancho de banda del bus disponible en el sistema se calcula restando al ancho de banda total del bus del sistema los requisitos de todas las aplicaciones que ya habían sido asignadas. Tan pronto como hay procesadores disponibles, el planificador atraviesa la lista de aplicaciones. Para cada aplicación que encaje en los procesadores disponibles, se calcula un valor de conveniencia mediante los datos anteriores. Al finalizar el recorrido de la lista, se selecciona la aplicación más conveniente para ser ejecutada durante el siguiente quantum. Esta selección favorece el aprovechamiento óptimo del bus. La segunda política de planificación se conoce como Quanta Window. La única diferencia con Latest Quantum es que en lugar de tener en cuenta los requisitos de ancho de banda del bus de cada hilo durante el último quantum del planificador, se utiliza la media de sus requisitos durante un limitado número de muestras anteriores. Esto tiene el efecto de suavizar los cambios repentinos en las transacciones del bus causados por una aplicación, filtrando ráfagas de corta duración u otras ráfagas causadas por eventos externos puntuales. Por otra parte, tiene la desventaja de reducir el tiempo de respuesta debido a cambios reales en el consumo de ancho de banda del bus. Para evaluar la efectividad de estas políticas, se experimenta con tres conjuntos de cargas de trabajo heterogéneas. El primer conjunto (figura 3) está formado por dos instancias de una aplicación de interés, cada una de las cuales requiere de dos procesadores, que se ejecutan junto a cuatro instancias del microbenchmark BBMA. En este conjunto, aplicaciones de interés coexisten con microbenchmarks que tienen una alta demanda del bus, permitiendo evaluar la eficiencia de las políticas en un bus ya saturado. Latest Quantum alcanza mejoras en el tiempo de respuesta que van desde un 4% hasta un 68%, obtieniendo una media del 41%. Las mejoras introducidas por Quanta Window varían entre un 2% y un 53%, con una media de un 31%. 9
3 Trabajo relacionado 3.1 Arquitectura de un procesador Durante la realización del proyecto se ha trabajado con el procesador con nombre comercial Intel Core 2 Duo processor T8100. Este procesador de doble núcleo pertenece a la línea de procesadores de 45 nm de Intel, con el identificador CPUID 23 y de nombre Penryn, que forma parte de la microarquitectura Core. 3.1.1 Introducción a la microarquitectura Core La microarquitectura Core fue presentada por Intel en el año 2006, como sucesora de la microarquitectura P6. Esta microarquitectura surge debido a las limitaciones y cuellos de botella que presentaba la microarquitectura Netburst, en la que estaban basados los procesadores Pentium 4/D, y que tuvo que ser abandonada por Intel. La evolución de las distintas microarquitecturas de Intel basadas en la arquitectura IA-32 queda reflejada en la figura 11. La microarquitectura Core se caracteriza por haber sido diseñada optimizada para multiprocesador. A diferencia de Netburst, se emplean frecuencias más bajas y se hace una utilización más eficiente de los ciclos de reloj disponibles. Esto hace que se reduzca el consumo energético al mismo tiempo que se incrementa la capacidad de proceso. Sin embargo, los procesadores basados en la microarquitectura Core no disponen de la tecnología Hyper-Threading presente en los procesadores Pentium 4. Esto es debido a que la microarquitectura Core es descendiente de la microarquitectura P6 utilizada por los Pentium Pro, Pentium II, Pentium III y Pentium M. 3.1.2 La jerarquía de memoria En la microarquitectura Core se dispone de una memoria caché de primer nivel (L1) para instrucciones y otra para datos por cada núcleo del procesador. En el caso de las versiones con dos núcleos, hay una única memoria caché de segundo nivel (L2) que es compartida por ambos, como 16 Figura 11: Evolución de la arquitectura IA-32
puede verse en la figura 12. Las características de la memoria caché del T8100 aparecen especificadas en la figura 13. Tipo de memoria caché Tamaño Correspondencia Tamaño de línea Política de reemplazo L1 instrucciones 2 x 32 KBytes Asociativa por conjuntos de 8 vías 64 bytes - L1 datos 2 x 32 KBytes Asociativa por conjuntos de 8 vías 64 bytes Write back L2 3072 KBytes Asociativa por conjuntos de 12 vías 64 bytes - Figura 13: Características de la memoria caché del T8100 Por otro lado, el bus frontal (FSB) que interconecta la caché L2 con la memoria principal funciona a 800 Mhz. Es importante comentar también que la cantidad total de memoria principal instalada en el sistema con el que se ha trabajado durante la realización del proyecto es de 3 GBytes. Para realizar la compartición de la caché L2, se emplea la tecnología de Intel Advanced Smart Cache, que permite a cada núcleo utilizar dinámicamente el 100% de la caché L2 disponible. Cuando un núcleo tiene unos requisitos mínimos de utilización de caché, el otro núcleo puede incrementar su porcentaje de uso de caché L2, reduciendo los fallos de caché e incrementando el rendimiento. Esto también permite la obtención de datos desde la caché a unas tasas de 17 Figura 12: Jerarquía de memoria en la microarquitectura Core
transferencia mayores. Por otro lado, cuando los dos núcleos requieren acceso a datos comunes, éstos son almacenados una sola vez de forma que ambos son capaces de acceder a los mismos. Todo esto permite obtener una mayor tasa de aciertos en la caché, una reducción del tráfico en el bus y una menor latencia de acceso a datos. En la figura 14 puede verse un esquema básico del funcionamiento de este sistema. 3.1.3 La unidad de monitorización del rendimiento La unidad de monitorización del rendimiento (PMU) es una infraestructura proporcionada por el hardware que permite la acumulación y obtención de datos de rendimiento del sistema a través de eventos que generan señales cuando se producen. Hay multitud de eventos hardware soportados, pero sólo unos pocos pueden ser contabilizados de forma simultánea, dependiendo de las características de la PMU. La información de la PMU puede obtenerse mediante la instrucción CPUID, pasando el valor 0x0A en el registro EAX (CPUID.0AH). Esta información será devuelta en los registros EAX, EBX y EDX. La monitorización de rendimiento arquitectural estará soportada si el identificador de versión es mayor que 0 (bits 7:0 del registro EAX). El procesador Intel Core 2 Duo processor T7700 y los procesadores posteriores basados en la microarquitectura Core (entre los que se encuentra el T8100), soportan la versión 2. Existen dos tipos de eventos, los eventos arquitecturales y los eventos no arquitecturales. •Los eventos arquitecturales son aquellos que se comportan de forma consistente entre microarquitecturas. Los bits a cero en el registro EBX tras ejecutar la instrucción CPUID.0AH indican que el evento está disponible. La lista de eventos arquitecturales y su correspondencia con el registro EBX queda reflejada en la figura 15. 18 Figura 14: Compartición de la caché L2
•Los eventos no arquitecturales son específicos de cada microarquitectura. En el caso de la microarquitectura Core, hay unos 400, especificados en el manual Intel(R) 64 and IA-32 architectures software developer's manual, volume 3B [5]. La unidad de monitorización de rendimiento está formada por colecciones de registros MSR (Model Specific Register). Los registros MSR pueden ser registros contadores y registros de control de los contadores. Se puede acceder a estos registros mediante las instrucciones RDMSR y WRMSR si se ejecutan en el nivel de privilegio 0 del procesador. Ciertos registros contadores también pueden ser leídos mediante la instrucción RDPMC, desde cualquier nivel de privilegio. Existen dos tipos de registros contadores, los de función fija y los de propósito general. Los eventos no arquitecturales sólo pueden ser recogidos por los contadores de propósito general. Los eventos arquitecturales pueden ser recogidos tanto por contadores de propósito general como por contadores de función fija. La información de los registros contadores que hay por cada núcleo del procesador se puede obtener mediante la instrucción CPUID.0AH. En el registro EDX, se obtiene el número de contadores de función fija (bits 4 a 0) y su tamaño en bits (bits 12 a 5). En el registro EAX, se obtiene el número de contadores de propósito general (bits 15 a 8) y su tamaño en bits (bits 23 a 16). En la microarquitectura Core, hay 3 contadores de función fija y 2 de propósito general por cada núcleo del procesador. El tamaño de cada contador es de 40 bits en ambos casos. Los registros contadores de función fija son llamados IA32_FIXED_CTRx. Cada uno de ellos se dedica a contar un evento arquitectural predefinido, que puede verse en la figura 16. 19 Figura 15: Lista de eventos arquitecturales
Los registros contadores de función fija comparten un único registro de control, llamado IA32_FIXED_CTR_CTRL. Su estructura puede verse en la figura 17. Este registro permite habilitar la cuenta en el nivel del sistema operativo, en el nivel de usuario, en todos los niveles o deshabilitar la cuenta para cada uno de los registros contadores. También permite habilitar y deshabilitar el envío de interrupciones cuando se produce un overflow en un contador. Los registros contadores de propósito general son llamados IA32_PMCx y los de control IA32_PERFEVTSELx. Cada registro contador de propósito general tiene asociado un registro de control. Los registros de control de propósito general son más complejos que el registro de control de función fija, debido a que necesitan información detallada del evento que se ha de programar. En la figura 18 puede verse la estructura del registro de control de propósito general. Event select y Unit mask identifican al evento. Los valores para los eventos arquitecturales se pueden consultar en la lista de eventos arquitecturales de la figura 15. Para los eventos no arquitecturales, se debe consultar el manual Intel(R) 64 and IA-32 architectures software developer's manual, volume 3B [5]. 20 Figura 17: Estructura del registro de control IA32_FIXED_CTR_CTRL Figura 16: Asociación de los contadores de función fija con eventos arquitecturales
También existen registros de control globales, que simplifican las operaciones más frecuentes en la programación de eventos. Esto es posible gracias a que con una sola instrucción sobre estos registros pueden verse involucrados todos los registros contadores o cualquier combinación de ellos, independientemente de si son contadores de función fija o de propósito general. Los registros de control globales son 3: •IA32_PERF_GLOBAL_CTRL habilita y deshabilita la cuenta de eventos mediante una única instrucción WRMSR. •IA32_PERF_GLOBAL_STATUS permite consultar los estados de overflow mediante una única instrucción RDMSR. •IA32_PERF_GLOBAL_OVF_CTRL permite limpiar los estados de overflow mediante una única instrucción WRMSR. 21 Figura 18: Estructura del registro de control IA32_PERFEVTSELx
3.2 Los contadores de rendimiento en el sistema operativo Los contadores de rendimiento aparecen por primera vez de forma oficial en la versión de Linux 2.6.31. Previamente había otros proyectos que permitían obtener la misma funcionalidad. Estos proyectos consistían por un lado en un parche que al aplicarlo al núcleo ofrecía una interfaz de llamadas al sistema y por otro lado en una biblioteca de usuario que implementaba la infraestructura de los eventos para las distintas arquitecturas, permitiendo trabajar de forma más sencilla. Entre estos proyectos destacan perfctr y perfmon2, proyectos con un varios años de desarrollo y experiencia. perfmon2 implementaba 12 llamadas al sistema que permitían trabajar con un sistema de contadores de rendimiento. La idea era que fuera introducido en la rama principal del núcleo, sin embargo nunca fue aceptado. En un intento por cambiar esto, el autor de perfmon2, Stéphane Eranian, propuso un nuevo parche que simplificaba la interfaz de llamadas al sistema, reduciendo el número de llamadas a 5, y pasando a llamarse perfmon3. Esta versión fue tenida en cuenta por varios desarrolladores del kernel, que comenzaron a desarrollar por su cuenta una nueva implementación que trata de alcanzar el mismo resultado final, pero basada en un concepto distinto en el que únicamente se añade una nueva llamada al sistema a la lista de llamadas existente. 3.2.1 La llamada al sistema sys_perf_event_open() El subsistema de contadores de rendimiento de Linux está concebido de forma que hay una única llamada al sistema para crear contadores. Para poder trabajar posteriormente con estos contadores, hay que hacer uso de las operaciones típicas del sistema de ficheros virtual de Linux (VFS), como por ejemplo operaciones de lectura o de bloqueo. El núcleo proporciona contadores virtuales de 64 bits, independientemente del tamaño de los contadores hardware subyacentes (cabe recordar que en el caso del T8100, son de 40 bits). Esto implica que debe encargarse del control de overflow cuando el tamaño del registro contador es más pequeño que el del contador virtual. La idea de hacer uso de contadores virtuales, es que los registros jamás queden expuestos al espacio de usuario. Para lograr este objetivo, se prescinde de una biblioteca de usuario, de modo que toda la infraestructura de los eventos para las diferentes arquitecturas se encuentra implementada en el propio núcleo. La figura 19 muestra la diferencia entre esta implementación y otras versiones. 22
Para poder acceder a los contadores de rendimiento en Linux, se hace uso de descriptores de fichero especiales, de modo que cada descriptor de fichero utilizado permite acceder a un contador virtual. Estos descriptores de fichero especiales se abren mediante la llamada al sistema sys_perf_event_open(). A partir de ese momento el evento programado comienza a ser contabilizado. De este modo, cada evento que se vaya a programar, requiere realizar una llamada al sistema sys_perf_event_open(). En caso de que el número de eventos hardware que se programen simultáneamente supere al número de registros de que dispone la unidad de monitorización del rendimiento (PMU) de la CPU, el núcleo multiplexará los contadores, de modo que todos los eventos puedan ser contabilizados. Esto se consigue realizando una planificación de tipo round-robin por grupos de eventos, de forma que los eventos agrupados son monitorizados al mismo tiempo (los eventos sin agrupar pueden verse también como grupos de eventos de un sólo miembro). De este modo, el número de eventos hardware por grupo no podrá superar al número de registros de que dispone la PMU del hardware. En la figura 20 puede verse un ejemplo de planificación circular con 4 grupos de eventos. En cada tic de reloj, el siguiente grupo de la cola pasa a ocupar los registros contadores. 23 Figura 19: API orientada a eventos contra API orientada a registros
Cuando se utiliza multiplexación, los datos recogidos deben ser posteriormente escalados, ya que un porcentaje de los valores se pierde. En la práctica, el factor de escala puede variar entre los distintos grupos de eventos que están siendo monitorizados. Esto dependerá en gran medida de si se permite o no contabilizar a varios grupos simultáneamente cuando se dispone de registros disponibles para todos ellos. En las figuras 21 y 22 puede verse un ejemplo de cada tipo. Así, la forma adecuada de calcular el factor de escala es a partir de los valores de multiplexación del evento tiempo total y tiempo activo: Factor deescala=ttotal tactivo Donde ttotal es el tiempo que la tarea o grupo de tareas a las que está asociado el evento ha sido monitorizado y tactivo es el tiempo durante el cual el evento o grupo de eventos ha sido contabilizado. El valor final obtenido para cada evento, deberá ser posteriormente multiplicado por su factor de escala correspondiente. Más adelante se verá cómo realizar la lectura del tiempo total y el tiempo activo. La interfaz de la llamada al sistema es la siguiente: 24 Figura 20: Planificación round-robin de grupos de eventos. Figura 21: Diferentes grupos de eventos pueden ser contabilizados simultáneamente (exclusive=0) Figura 22: No está permitida la monitorización simultánea de grupos de eventos (exclusive=1)
int sys_perf_event_open(struct perf_event_attr *attr_uptr, pid_t pid, int cpu, int group_fd, unsigned long flags) La llamada al sistema devuelve el nuevo descriptor de fichero. El descriptor de fichero puede ser utilizado con las llamadas al sistema habituales: •read() permite leer el contador. •close() cierra el descriptor. •fcntl() puede ser utilizada para establecer el modo de bloqueo. •poll() espera un evento en un descriptor de fichero. El parámetro de tipo perf_event_attr define cómo se comportará el evento. Es una estructura compleja, con un considerable número de campos. struct perf_event_attr { __u32 type; __u32 size; __u64 config; union { __u64 sample_period; __u64 sample_freq; }; __u64 sample_type; __u64 read_format; __u64 disabled : 1, inherit : 1, pinned : 1, exclusive : 1, exclude_user : 1, exclude_kernel : 1, exclude_hv : 1, exclude_idle : 1, mmap : 1, comm : 1, freq : 1, inherit_stat : 1, enable_on_exec : 1, task : 1, watermark : 1, precise_ip : 2, mmap_data : 1, sample_id_all : 1, __reserved_1 : 45; union { __u32 wakeup_events; __u32 wakeup_watermark; }; __u32 bp_type; union { __u64 bp_addr; __u64 config1; }; union { __u64 bp_len; __u64 config2; }; }; Los tres campos básicos de la estructura perf_event_attr que hay que definir para programar un evento son type, size y config. •type indica el tipo de evento que se ha de monitorizar. Los eventos pueden ser de varios 25
4 Experimentos y datos 4.1 Desarrollo de una herramienta para la toma de datos Para poder realizar la caracterización de las aplicaciones, es necesario disponer de alguna herramienta que permita la recogida y el tratamiento de los datos de los contadores de rendimiento. En este caso, la herramienta implementada obtendrá los datos derivados de la ejecución de los benchmarks. 4.1.1 Diseño El diseño de la herramienta de caracterización está basado en un modelo que utiliza pequeñas aplicaciones en espacio de usuario, las cuales trabajan con ficheros de datos. Estas aplicaciones pueden ser utilizadas de forma independiente, pasando de forma adecuada los parámetros a través de la línea de órdenes. Esto ha sido de gran utilidad durante la realización del proyecto para poder ensayar con los datos generados anteriormente sin tener que volver a ejecutar todos los pasos. Sin embargo, por simplicidad y automatización del proceso, fue necesario escribir un guión (también conocido en inglés como script) en Bash, que se encarga de realizar todos los pasos de forma ordenada, mediante el lanzamiento parametrizado del resto de aplicaciones. El esquema puede verse en la figura 24. 32 Figura 24: Esquema de ejecución de un benchmark
4.1.2 Lanzamiento de las aplicaciones runbench es un guión escrito en Bash que se encarga de coordinar el lanzamiento del resto de aplicaciones con los parámetros adecuados. Su utilización a través de la línea de órdenes es sencilla, requiriendo únicamente el nombre del benchmark que se debe lanzar. Opcionalmente, es posible indicar mediante la opción --no-plot que no se desea mostrar la gráfica con gnuplot al finalizar la ejecución, lo cual es útil cuando se quiere programar la ejecución de varios benchmarks de larga duración para su análisis posterior. $ ./runbench [--no-plot] nombre_benchmark En primer lugar, el guión debe comprobar si el benchmark indicado se encuentra en su lista de benchmarks conocidos, para asignarle los parámetros que necesita para iniciar su ejecución. La configuración de los parámetros de entrada de los benchmarks está basada en la información de la página del proyecto Mult2Sim [17]. En caso afirmativo, se lanza la aplicación test_bench, que recibe el nombre del fichero ejecutable (en este caso el benchmark) y la lista de argumentos del mismo. Esta aplicación escribe en un fichero binario los datos de la ejecución del benchmark. Una vez finalizada la ejecución de test_bench, se lanza la aplicación parser. Esta aplicación recibe el nombre del fichero binario de datos anterior y transforma los datos en texto, en un formato por columnas que puede ser interpretado por gnuplot. A continuación se genera un fichero de configuración de gnuplot para el benchmark anteriormente ejecutado y finalmente se lanza el programa gnuplot, para que muestre la gráfica con los resultados, siempre y cuando no se haya seleccionado la opción --no-plot. 4.1.3 Recogida de datos de los benchmarks test_bench es la aplicación de mayor relevancia. Tiene como misión el lanzamiento de un benchmark y la recogida de los datos proporcionados por los contadores de rendimiento a lo largo de toda su ejecución. Para ello, se hace uso del sistema de contadores de rendimiento que ofrece Linux. Definición del evento En primer lugar hay que definir el evento que se pretende contabilizar. Para que sea posible caracterizar las aplicaciones, debe escogerse un evento que permita obtener el patrón de accesos a memoria en función del tiempo de referencia. Debido a que todos los accesos a memoria que se realizan han de pasar por el primer nivel de caché, se ha optado por el evento L1D_ALL_REF que aparece en el manual de Intel [5]. Este evento cuenta todas las referencias de cualquier tipo a la memoria caché de datos de primer nivel, incluyendo lecturas y escrituras. L1D_ALL_REF es un evento no arquitectural que hay que codificar como RAW, lo cual tiene como desventaja una mayor complejidad, debido a la escasa abstracción que ofrece. Según la lista de eventos no arquitecturales de la microarquitectura Core que aparece en el manual de Intel, este evento se codifica como Event Select = 43H y Unit Mask = 01H. Si se observa el formato del registro IA32_PERFEVTSELx (figura 18), se corresponde con los bits 7-0 y 15-8 del 33
registro, respectivamente. Así, el evento codificado quedaría como 0x0143. Sin embargo, faltan otros bits del registro que también hay que configurar, como la habilitación inicial (EN = 1) o si se desea contabilizar en modo núcleo (OS = 1) o usuario (USR = 1). Otra opción más sencilla es emplear la biblioteca auxiliar libpfm4 [14], escrita por el autor de perfmon2 para funcionar con el sistema de contadores de rendimiento de Linux. Aunque esta biblioteca no ha sido estudiada en este proyecto para descubrir sus posibilidades, se han empleado dos de las utilidades de ayuda que ofrece. Por un lado, mediante showevtinfo, es posible descubrir los eventos soportados disponibles: $ ./libpfm-4.1.0/examples/showevtinfo | less #----------------------------- IDX : 23068719 PMU name : core (Intel Core) Name : L1D_ALL_REF Equiv : None Flags : None Desc : All references to the L1 data cache Code : 0x143 Modif-00 : 0x00 : PMU : [k] : monitor at priv level 0 (boolean) Modif-01 : 0x01 : PMU : [u] : monitor at priv level 1, 2, 3 (boolean) Modif-02 : 0x02 : PMU : [e] : edge level (boolean) Modif-03 : 0x03 : PMU : [i] : invert (boolean) Modif-04 : 0x04 : PMU : [c] : counter-mask in range [0-255] (integer) : Por otro lado, una vez localizado el evento L1D_ALL_REF, se averigua su codificación, mediante la utilidad evt2raw: $ ./libpfm-4.1.0/perf_examples/evt2raw -v L1D_ALL_REF r530143 core::L1D_ALL_REF:e=0:i=0:c=0:u=1:k=1 La codificación RAW del evento L1D_ALL_REF que se ha obtenido es 0x530143. Se puede observar que por defecto aparecen activados los bits 22 (Enable counters), 20 (APIC interrupt enable), 17 (Operating system mode) y 16 (User mode) del registro IA32_PERFEVTSELx. Funcionamiento de la aplicación test_bench está escrita en C. Esta aplicación recibe como parámetros de entrada la ruta del ejecutable del benchmark junto a sus argumentos, para poder después ejecutarlo. En primer lugar se inicializa la estructura perf_event_attr con la definición del evento. ... #define L1D_ALL_REF 0x00530143 ... int main(int argc, char **argv) 34
{ ... struct perf_event_attr attr_L1DAllRef = { .type = PERF_TYPE_RAW, .config = L1D_ALL_REF, ... A continuación, la aplicación se divide en dos procesos mediante la llamada fork() y el hijo se suspende voluntariamente. ... childpid = fork(); if (childpid == -1) { // Error ... } else if (childpid == 0) { // Hijo kill (getpid(), SIGSTOP); ... El padre espera a que el hijo se suspenda mediante un pequeño bucle de espera activa, ya que posteriormente ha de enviarle una señal para despertarlo, y ésta podría perderse. El motivo de sincronizar al padre y al hijo es debido a que al ser dos procesos, el orden de ejecución lo establece el planificador y no es posible controlarlo ni hacer suposiciones de quién llegará antes a determinado punto del código. ... } else { // Padre while (waitpid(childpid, NULL, WUNTRACED) == 0) usleep(1000); ... El proceso padre será el encargado de realizar la monitorización. Para que ésta pueda realizarse de forma más precisa, el padre intenta cambiar su tipo de proceso por uno de tiempo real. Esto sólo es posible si el proceso es lanzado con permisos de superusuario. ... if (geteuid() == 0) sched_setscheduler(0, SCHED_FIFO, &p); ... Por otro lado, también prepara en modo escritura el fichero donde se van a volcar los datos de la monitorización. ... if ( (fd_output = open("/tmp/bench_output.data", O_WRONLY | O_CREAT | O_TRUNC, 0644)) == -1) { perror("No se pudo abrir el fichero de salida"); kill (childpid, SIGTERM); exit(EXIT_FAILURE); } ... A continuación el padre programa el evento anteriormente definido con la llamada al sistema sys_perf_event_open() y envía al hijo la señal SIGCONT para despertarlo. 35
... fd_L1DAllRef = sys_perf_event_open(&attr_L1DAllRef, childpid, -1, -1, 0); if (fd_L1DAllRef < 0) { perror("No se pudo obtener el descriptor del PMC"); kill (childpid, SIGTERM); exit(EXIT_FAILURE); } kill (childpid, SIGCONT); ... El hijo cambia su mapa de memoria por el del benchmark mediante la llamada exec(), haciendo uso de los parámetros de entrada que recibió la aplicación. ... if (execv(argv[1], &argv[1]) < 0) { perror("No se pudo ejecutar el programa"); exit(EXIT_FAILURE); } ... Para poder medir los valores intermedios, el padre entra en un bucle, en el cual obtiene tanto el valor del contador como el del tiempo de ejecución del proceso hijo, escribe los resultados en el fichero de salida y se suspende durante un tiempo previamente definido (por ejemplo, de 100 ms). Este bucle se repite hasta que el proceso hijo termina. Debido a que la aplicación trabaja en espacio de usuario, no es posible obtener los datos del tiempo de ejecución del benchmark directamente de su estructura del núcleo task_struct. Por este motivo, hubo que plantear varias alternativas, hasta que se alcanzó la mejor forma posible. En primer lugar se planteó la posibilidad de medir el tiempo mediante el uso de la función gettimeofday() definida en el fichero de cabecera sys/time.h. Esta función devuelve en una estructura de tipo timeval el número de segundos y microsegundos desde el 1 de enero de 1970. Con este valor, es posible calcular el tiempo que ha transcurrido entre una lectura y la siguiente. ... read(fd_L1DAllRef, &cont_L1DAllRef, sizeof(u64)); gettimeofday(&ts, NULL); ... Este método fue descartado, ya que no se obtiene el tiempo de ejecución, sino el tiempo transcurrido. Incluso aunque las experiencias se realicen con la mínima carga posible en el sistema y se trabaje con un procesador de doble núcleo, no se puede dar por hecho que el planificador va a mantener al benchmark casi permanentemente en ejecución. La segunda opción planteada, sí permite obtener el tiempo de ejecución de un proceso. Para ello, se hace uso de la función get_proc_stats() de la biblioteca auxiliar libproc [19]. Esta función obtiene en una estructura de tipo proc_t la información del proceso, a través del sistema de ficheros del núcleo procfs (montado en /proc). A partir de esta información, es posible calcular el tiempo de ejecución como la suma del tiempo de usuario y el tiempo de núcleo. ... read(fd_L1DAllRef, &cont_L1DAllRef, sizeof(u64)); 36
get_proc_stats(childpid, &p); uk_time = p.utime + p.stime; ... Si bien este método es capaz de obtener el tiempo de ejecución del benchmark, puede plantear un inconveniente. Al estar trabajando en espacio de usuario, y debido a que la lectura del valor del evento y del tiempo de ejecución del proceso no son una operación atómica, es posible que el planificador expulse al proceso monitorizador entre la ejecución de ambas operaciones. Esto haría que ese muestreo y el inmediatamente posterior, no mostraran una relación tiempo de ejecución / valor del contador correcta. Finalmente, se descubre un tercer método que soluciona los problemas anteriormente planteados, gracias al estudio más detallado de la interfaz del sistema de contadores de rendimiento de Linux. Para ello, hay que configurar el formato de lectura en la estructura perf_event_attr. ... struct perf_event_attr attr_L1DAllRef = { ... .read_format = PERF_FORMAT_TOTAL_TIME_RUNNING }; ... De este modo, es posible obtener mediante una única llamada read() el valor del contador y el tiempo de ejecución del proceso. ... read(fd_L1DAllRef, cont_L1DAllRef, sizeof(u64) * 2); ... Hay que destacar que no se han observado diferencias significativas en los resultados con respecto al método anterior usando un período de muestreo de 100 ms. A continuación se muestra cómo queda el bucle de lectura de valores intermedios implementando este último método. ... do { read(fd_L1DAllRef, cont_L1DAllRef, sizeof(u64) * 2); if (write(fd_output, cont_L1DAllRef, sizeof(u64) * 2) != sizeof(u64) * 2) { perror("No se pudo escribir en el fichero de salida"); kill (childpid, SIGTERM); exit(EXIT_FAILURE); } usleep(PERIODO); waitpid(childpid, &status, WNOHANG); } while (!WIFEXITED(status)); ... El tiempo transcurrido entre las muestras tomadas por el padre es aproximado, ya que dependerá del tiempo que pase desde que se despierta y pasa a la cola de preparados hasta que el planificador de procesos lo pase a ejecución. Una vez se ha terminado, se vuelven a leer los contadores para mostrar por pantalla los valores finales. Esta última lectura no es tenida en cuenta para las estadísticas. 37
... read(fd_L1DAllRef, cont_L1DAllRef, sizeof(u64) * 2); printf("Valores finales:\n"); printf(" L1D_ALL_REF: %llu\n", cont_L1DAllRef[1]); close(fd_L1DAllRef); close(fd_output); exit(EXIT_SUCCESS); } } De este modo, ya es posible emplear la aplicación parser para pasar los datos al formato que utiliza gnuplot y visualizar así los resultados en una gráfica. 4.1.4 Selección y conversión de los datos Antes de realizar las observaciones, hay que preparar los datos para poder representarlos en una gráfica. De esto se encarga la aplicación parser, escrita en C. En un primer momento, se trabajó con una única versión para todos los tiempos de muestreo. En dicha versión, parser recibe como parámetro el nombre del fichero que contiene los datos en formato binario. Mediante un bucle, lee cada valor del contador y del tiempo de ejecución. ... valor_ant = 0; tiempo_ant = 0; while ( (count = read(from_fd, &valor, sizeof(u64))) > 0 && (count2 = read(from_fd, &tiempo, sizeof(u64))) > 0) { ... En primer lugar, se comprueba si se dan pares de valores repetidos de tiempo de ejecución y valor del contador y se descartan, teniendo cuidado de no eliminar el primer valor. Los valores repetidos indican que el planificador no ha pasado a ejecución al benchmark entre dos muestreos, y no aportan nada en este caso. ... if (tiempo == tiempo_ant && valor == valor_ant && tiempo > 0) continue; ... A continuación, el tiempo de ejecución es convertido de nanosegundos a segundos, y al valor del contador se le resta el inmediatamente anterior para calcular el valor contabilizado durante su tiempo de muestreo. Estos dos valores son mostrados por la salida estándar, en formato de dos columnas. ... printf("%.5f %llu", tiempo / 1000000000.0, valor – valor_ant); ... Como extra, se han añadido dos valores más que no son utilizados por la gráfica, pero que han servido de ayuda durante la realización del proyecto. Estas dos últimas columnas muestran el valor total acumulado por el contador y la diferencia de tiempo con su valor inmediatamente anterior, 38
pasado a milisegundos. ... printf(" %llu %.2f\n", valor, (tiempo - tiempo_ant) / 1000000.0); ... Así, los datos se muestran en un total de 4 columnas. #t(s) #Dif_evento #Total_evento #Dif_t(ms) 0.09803 157012455 157012455 98.03 0.19803 138855760 295868215 100.00 0.29805 101751944 397620159 100.02 0.39805 167272634 564892793 100.00 0.49804 137905323 702798116 99.99 ... Después de mostrarse cada fila de datos, se actualizan los valores de valor_ant y tiempo_ant. ... valor_ant = valor; tiempo_ant = tiempo; } ... Esta aplicación funciona bien para las mediciones realizadas con valores de suspensión en el bucle de test_bench de 100 ms y 5 s. Sin embargo, al convertir los datos de la ejecución que emplea un valor de referencia de 1 ms, se observan valores del contador anormalmente altos, que desvirtúan los resultados. Un análisis más en profundidad muestra que hay casos en los que la aplicación de monitorización pasa largos períodos de tiempo con respecto al tiempo de referencia sin recoger datos, lo que provoca que se acumulen picos de valores para la siguiente lectura. Al observar con mayor detenimiento los datos con un valor de referencia de 100 ms y 5 s, se observa que en el caso de los 100 ms también se da, aunque rara vez, de forma aleatoria en alguna medición. Para mitigar esto, se modifica la aplicación test_bench de modo que el proceso de monitorización se planifique como proceso de tiempo real, como pudo verse en el punto 4.1.3. También se implementa una segunda versión de la aplicación de conversión, adaptada específicamente para los datos recogidos con un valor de referencia de 1 ms y 100 ms. En esta versión, la diferencia se encuentra en que antes de imprimir los datos, se comprueba si el tiempo de ejecución del benchmark durante el período de muestreo ha superado un cierto umbral. En caso afirmativo, se introduce un comentario al principio de la línea. Esto evita que dicho dato sea mostrado posteriormente en la gráfica, pero permitiendo realizar observaciones fuera de la misma. ... #ifndef T_5S if (tiempo - tiempo_ant > UMBRAL) printf("# "); #endif ... El valor del umbral escogido tras realizar varias observaciones ha sido de un 50% sobre el valor de referencia, que es de 1'5 ms para 1 ms y 150 ms para 100 ms. El objetivo es obtener un equilibrio 39
entre datos descartados y valores corrientes, ya que el período de muestreo real suele estar ligeramente por encima del valor de referencia en el caso de 1 ms. Este valor probablemente podría ser afinado con un análisis estadístico más detallado. ... #ifdef T_1MS #define UMBRAL 1500000 #elif T_100MS #define UMBRAL 150000000 ... Debido a que los datos se muestran por la salida estándar, es preciso redirigir la salida a un fichero durante la ejecución, para poder después trabajar con ellos. 4.1.5 Representación de la información Para representar la información, se utilizan gráficas de dos dimensiones. En el plano, el eje de abscisas representa el tiempo de ejecución del benchmark, mientras que el eje de ordenadas muestra el número de eventos producidos durante el período de muestreo. En el ejemplo de la figura 25, puede verse la representación del benchmark 403.gcc en una fase anterior del proyecto, en la que se estuvo estudiando cómo trabajar con más de un evento de forma simultánea. El tiempo de referencia es de 100 ms. El programa empleado para este fin es gnuplot. Para poder utilizarlo simplemente hay que indicar el fichero de configuración .plt previamente generado por el guión runbench. $ gnuplot -p 403.gcc.plt 40 Figura 25: Ejemplo de ejecución de un benchmark con 3 eventos diferentes
En este fichero de configuración se indican varios parámetros como el título, el nombre de los ejes o el fichero de datos que se ha de leer junto con la relación eje – columna de datos que se desea mostrar. set title "403.gcc" set xlabel "Tiempo, t (s)" set ylabel "nº de eventos producidos" set grid set autoscale fix plot \ "403.gcc.dat" using 1:2 title "L1_D_ALL_REF" with linespoints, \ "403.gcc.dat" using 1:3 title "L1-dcache-read-misses" with linespoints, \ "403.gcc.dat" using 1:4 title "L1-dcache-write-misses" with linespoints También es posible configurar gnuplot para generar ficheros de imágenes, en lugar de mostrar directamente la gráfica. Para ello hay que añadir dos opciones extra justo antes de la opción plot, indicando las propiedades de la imagen y el fichero de salida. ... set terminal pngcairo size 1272,384 font "LiberationSans-Regular,10" set output "403.gcc.png" ... Este paso también ha sido automatizado por medio del guión runbench. 41
483.xalancbmk Parámetros: “-v t5.xml xalanc.xsl” El comportamiento de accesos a memoria en este benchmark puede dividirse en 3 partes. Con un período de 1 ms, los accesos a memoria se vuelven muy agresivos. En la figura 38, se puede observar que en la primera zona los accesos a memoria se muestran irregulares. Después se pasa a una segunda zona con un nivel de accesos a memoria más alto. Por último, el resto de la ejecución se muestra con accesos a memoria muy cambiantes, aunque con la inmensa mayoría de sus valores acotados entre 0 y 2'2·10 eventos.⁶ La figura 39 muestra los resultados con un período de 100 ms. En la primera zona, se observa cómo se repite un patrón en el que los accesos a memoria crecen rápidamente para después pasar a decrecer en forma de curva. Después de esta zona, aparece otra zona con gran cantidad de accesos a memoria. La última zona, muestra importantes fluctuaciones, aunque permite ver cómo los accesos a memoria crecen lentamente a lo largo del resto de la ejecución. 48 Figura 37: 473.astar, 5 s Figura 38: 483.xalancbmk, 1 ms
La figura 40 muestra la gráfica con un período de 5 s. Los resultados se muestran de forma más suave que en la gráfica de 100 ms. Se puede observar también la línea que sigue la pendiente de la última zona, sin fluctuaciones pronunciadas. Benchmarks de coma flotante 436.cactusADM Parámetros: “benchADM.par” El tiempo de ejecución de este benchmark es elevado, con una media superior a los 34 minutos. Al comienzo crece drásticamente el número de accesos a memoria, para luego reducirse rápidamente y volver a aumentar lentamente durante el resto de la ejecución. Con un período de 1 ms, el número de muestreos supera los 2 millones. Además, los accesos a memoria se muestran muy agresivos. Debido a esto, resulta complicado apreciar los detalles en la figura 41. 49 Figura 39: 483.xalancbmk, 100 ms Figura 40: 483.xalancbmk, 5 s
A continuación se ofrecen los resultados empleando un período de 100 ms. En la figura 42 puede apreciarse el comportamiento de los accesos a memoria. A lo largo de la ejecución, las fluctuaciones disminuyen a medida que aumenta la tasa de accesos a memoria. En la figura 43 puede observarse la gráfica con un período de 5 s. El progreso de los accesos a memoria se encuentra plasmado de forma más clara. 50 Figura 41: 436.cactusADM, 1 ms Figura 42: 436.cactusADM, 100 ms Figura 43: 436.cactusADM, 5 s
444.namd Parámetros: “--input namd.input --iterations 38 --output namd.out” En este benchmark, puede observarse claramente un patrón de accesos de unos 21 segundos, que se repite a lo largo de toda la ejecución. La figura 44 muestra la gráfica con un período de 1 ms, en la que el patrón se repite en 38 ocasiones. En la figura 45 se emplea un período de 100 ms, y también puede observarse cómo el patrón de accesos a memoria se repite 38 veces. En la figura 46 se muestran los resultados empleando un período de 5 s. Un período de muestreo tan grande comparado con el tamaño del patrón, hace que sea complicado identificarlo. 51 Figura 44: 444.namd, 1 ms Figura 45: 444.namd, 100 ms
447.dealII Parámetros: “23” En este benchmark de coma flotante, aparece un patrón de accesos a memoria con un período creciente a lo largo de toda su ejecución. En la figura 47 se puede observar este patrón. Con un tiempo de ejecución de 1 ms, los accesos a memoria ofrecen fluctuaciones que mantienen un comportamiento similar en cada aparición del patrón. Con un período de 100 ms, el patrón puede verse de forma más clara en la figura 48, ya que las fluctuaciones son menores. 52 Figura 46: 444.namd, 5 s Figura 47: 447.dealII, 1 ms
En la figura 49, se muestran los resultados con un período de 5 s. El patrón se puede seguir observando, sin embargo sólo puede distinguirse con claridad cuando el período del mismo aumenta lo suficiente. 450.soplex Parámetros: “-m3500 ref.mps” En este benchmark, los accesos a memoria son irregulares al principio de la ejecución, para luego pasar a una zona con importantes cambios muy rápidos en el número de accesos a memoria, cuya media se mantiene estable. En la figura 50, puede verse el resultado tras realizar la ejecución con un período de 1 ms. Los accesos a memoria mantienen un comportamiento muy agresivo durante toda la ejecución. Es posible distinguir los cambios que hay en la zona inicial del resto de la ejecución. 53 Figura 48: 447.dealII, 100 ms Figura 49: 447.dealII, 5 s
La figura 51 muestra la ejecución con un período de 100 ms. La zona inicial con diferentes requisitos de accesos a memoria ahora puede distinguirse con mucha más claridad, mientras que el resto de la ejecución muestra una línea que se mantiene más o menos estable, dentro de las fluctuaciones que se producen a lo largo de la misma. En la figura 52 se muestran los accesos a memoria con un período de 5 s. Aquí se puede distinguir la zona inicial, con dos picos importantes de accesos a memoria, mientras que la línea estable de accesos a memoria puede verse mucho más suave. 54 Figura 50: 450.soplex, 1 ms Figura 51: 450.soplex, 100 ms
465.tonto Parámetros: ninguno. El último benchmark de coma flotante, muestra un patrón que se repite en 8 ocasiones a lo largo de la ejecución, tras una zona inicial en la que los accesos a memoria tienen una media alta. Por otro lado, el patrón se divide en una zona con un alto número de accesos a memoria y en otra zona con un menor número de accesos. La figura 53 muestra los accesos a memoria haciendo uso de un período de 1 ms. La zona inicial muestra las importantes fluctuaciones que se producen en los requisitos de accesos a memoria. Curiosamente, la repetición del segundo patrón también tiene una zona con un comportamiento más agresivo, que lo diferencia del resto de repeticiones. En la figura 54, con un período de 100 ms, se pueden observar las zonas alta y baja del patrón con un aspecto más definido, ya que las fluctuaciones son menores. 55 Figura 52: 450.soplex, 5 s Figura 53: 465.tonto, 1 ms
Finalmente, en la figura 55 se muestran los resultados empleando un período de 5 s. El patrón se muestra en este caso con un trazo más sencillo. 56 Figura 54: 465.tonto, 100 ms Figura 55: 465.tonto, 5 s
5 Conclusiones y trabajo futuro El trabajo realizado en este proyecto, muestra una posible forma de realizar la caracterización de aplicaciones. Esta caracterización se confecciona fuera de línea, es decir, los resultados pueden ser analizados en una gráfica una vez finalizada la ejecución de cada prueba. A la hora de seleccionar el evento para monitorizar, ha habido que consultar y utilizar un evento concreto ofrecido por la microarquitectura en la que está basada el procesador con el que se han realizado las experiencias. Esto dificulta la portabilidad con procesadores de otras microarquitecturas o fabricantes. La implementación de los contadores de rendimiento del sistema operativo, ofrece una abstracción para determinados tipos de eventos. Lamentablemente, el evento que se ha utilizado en este proyecto, no se encuentra disponible en esta lista de eventos generalizada. Durante el desarrollo de la herramienta, se han encontrado problemas para que el período de monitorización se mantuviera estable. Esto es debido a que el proceso monitorizador es un proceso más que debe competir por la CPU. Se ha tratado de minimizar este efecto aumentando la prioridad del proceso y ajustando la aplicación de tratamiento de datos. Sin embargo, para lograr una mayor precisión, sería conveniente realizar la monitorización desde el propio núcleo. Es posible observar que cuanto menor es el período de muestreo, se producen cambios más bruscos en los requisitos de accesos a memoria de la aplicación, que generan picos y caídas. Los picos altos son muy importantes, ya que pueden alcanzar el límite de la capacidad de la memoria, pasando desapercibidos cuando se utilizan tiempos de referencia superiores. Esto debería ser estudiado con mayor detenimiento en un trabajo futuro. Como trabajo futuro se podría realizar un planificador de procesos que tome decisiones guiadas por el comportamiento de las aplicaciones, es necesario hacer la caracterización en tiempo real. Para ello, el planificador deberá emplear una ventana de tiempo en la que recoja la evolución de los accesos a memoria o la utilización del ancho de banda del bus de memoria. Tras aplicar un algoritmo que haga uso de esta información, tendrá que decidir qué grupos de tareas deben pasar a ejecución en cada quantum de tiempo de planificación. Otra alternativa de planificador de procesos que se podría implementar sería un planificador que utilizara la información que haya sido obtenida anteriormente de la ejecución de las aplicaciones. Este planificador conocería de antemano el comportamiento futuro de la aplicación, con lo que podría mejorar las decisiones de planificación. El método para caracterizar las aplicaciones empleado en este proyecto, es extensible a otros tipos de evento. En este caso, se ha utilizado un evento para caracterizar las aplicaciones en cuanto a accesos a memoria, pero en un trabajo futuro habría que estudiar qué otros eventos pueden ser de utilidad, como el ancho de banda del bus de memoria utilizado. 57