VI SIMPOSIO ARGENTINO DE TECNOLOGIA EN COMPUTACION: AST-PAR002 1 Generación de Trazas Multihilo Simultáneo Augusto J. Vega, José L. Hamkalo y Bruno Cernuschi Frı́as Facultad de Ingenierı́a, Universidad de Buenos Aires, {ajvega,jhamkal}@fi.uba.ar Resumen— En este trabajo se presenta una herramienta para la generación y captura de trazas de aplicaciones multihilo, para su uso en simulación de memorias cache en ambientes multihilo simultáneo. El ambiente desarrollado es construido a partir de modificaciones aplicadas a una herramienta de debugging y profiling llamada Valgrind. La herramienta utiliza como entrada el código ejecutable de la aplicación a ser procesada, la cual es traducida e instrumentada en forma dinámica. El ambiente propuesto permite la captura de trazas de aplicaciones paralelas reales, incluyendo aplicaciones comerciales de las cuales no se dispone del código fuente. Se obtuvo una granularidad muy fina, en la práctica de hasta una referencia por cada conmutación de hilo de ejecución activo. Se implementó un mecanismo para el almacenamiento de las trazas en forma comprimida y con un formato simple. Palabras clave— Traza, Multihilo, Cache. I. I NTRODUCCI ÓN En los últimos años, una nueva arquitectura de procesadores conocida como multihilo simultáneo o SMT (Simultaneous Multithreading) [2], ha crecido en relevancia y ha sido incorporada en los microprocesadores actuales, tales como los procesadores Intel Pentium IV HT [3]. Dado que es una tecnologı́a muy reciente, son muy escasas o inexistentes en la práctica, herramientas que simulen o aporten los datos necesarios para realizar simulaciones de estos ambientes multihilo simultáneo. En este trabajo presentamos una forma para la obtención de los mencionados datos y la forma de utilizarlos para la emulación de ambientes SMT en la simulación de jerarquı́as de memorias. El ambiente propuesto se ha construido a partir de modificaciones aplicadas a una herramienta de debugging y profiling de dominio público, llamada Valgrind [14]. A. El procesamiento multihilo simultáneo (SMT) Una arquitectura uniprocesador tradicional (por ejemplo, superescalar) explota el paralelismo a nivel de instrucción (ILP) [7] para lograr un mejor desempeño en la ejecución de los procesos. Sin embargo, este grado de paralelismo suele ser pobre en la mayorı́a de los casos, dando como resultado un desperdicio en el aprovechamiento de los recursos del procesador. En adición al problema del bajo nivel de ILP, un procesador superescalar también se caracteriza por desperdiciar capacidad de cómputo, permaneciendo durante algunos ciclos de reloj en estado ocioso. En ocasiones, la ejecución del flujo de instrucciones suele bloquearse debido a, entre otras cosas, malas predicciones de los saltos, desaciertos en la memoria caché de instrucciones, operaciones de E/S, etc., lo que se traduce indefectiblemente en un procesador ocioso, a la espera de una instrucción a ejecutar. Resumiendo, un procesador puede sufrir de: Desperdicio Horizontal: debido al bajo nivel de ILP que impide, para un ciclo de reloj, usar eficientemente los recursos (unidades funcionales) disponibles. Desperdicio Vertical: debido al bloqueo o latencias en las instrucciones ejecutadas, con lo cual, el procesador debe permanecer ocioso. Desde hace varios años, los sistemas se software se basan en la utilización de múltiples procesos livianos o hilos de ejecución. En este caso, existen varios flujos de instrucciones a ejecutar que comparten un mismo espacio de direcciones. El uso de un procesador superescalar como el descrito anteriormente resulta en un serio cuello de botella, ya que la ejecución de los hilos es serializada en el mismo. Para aprovechar el paralelismo de los sistemas multihilo, se introdujeron modificaciones a dichos procesadores, de manera tal que sean capaces de tomar instrucciones de los diferentes hilos, e intercalar la ejecución de las mismas. Ası́, surgieron dos esquemas, conocidos como Coarse-Grained Multithreading (CMT) y Fine-Grained Multithreading (FMT). En el primer caso, la ejecución de los flujos se alterna cuando el que estaba en ejecución incurre en alguna penalidad, tal como en el caso de un desacierto en la memoria caché L2 [8], mientras que en el esquema FMT, los flujos se alternan en cada instrucción ejecutada. El objetivo de estas polı́ticas consiste en reducir el desperdicio de ciclos de reloj: si un flujo se bloquea, entonces el procesador realiza un cambio de contexto y continúa con la ejecución del otro flujo. Sin embargo, solo resuelven un problema (desperdicio vertical), pero no mejoran el uso de las unidades funcionales por cada ciclo de reloj. En los últimos años se propuso un nuevo esquema, que es en sı́ mismo una variación del FMT, y que se llamó SMT (Simultaneous Multithreading). La solución planteada consiste en reducir los desperdicios de tiempo y uso de recursos que señalamos anteriormente (o sea, se busca minimizar tanto el desperdicio horizontal como también el desperdicio vertical). Para evitar que el procesador permanezca ocioso, las instrucciones a ejecutar se toman de varios flujos simultáneamente, de manera tal que, ante el bloqueo de uno de ellos, se pueda continuar con la ejecución de alguno de los otros. En realidad, la anterior ya es una caracterı́stica de la polı́tica FMT. La diferencia está en que un esquema SMT busca maximizar, para cada ciclo de reloj, la utilización de los recursos del procesador. Si el flujo (o hilo) en ejecución presenta un alto nivel de ILP, entonces ese paralelismo permite explotar al máximo la utilización de las unidades funcionales para un ciclo de reloj. Por otra parte, si varios flujos presentan cada uno un bajo nivel de ILP, entonces pueden ser ejecutados simultáneamente, para maximizar el aprovechamiento de los recursos. B. Los procesadores de alta performance y el sistema de memoria Las memorias caché han jugado un rol central en el aumento sostenido de la performance de las computadoras del alto rendimiento [6]. Las mejoras en la arquitectura del procesador y la tecnologı́a han contribuido en forma pareja durante las últimas dos décadas como las fuerzas principales para conseguir niveles sin precedentes en el VI SIMPOSIO ARGENTINO DE TECNOLOGIA EN COMPUTACION: AST-PAR002 funcionamiento de los procesadores de propósito general [8]. Los procesadores de hoy en dı́a utilizan pipelines muy profundos, implementándose con tecnologı́as de integración submicrón decrecientes y como ya se expuso anteriormente son capaces de manejar múltiples hilos de ejecución en forma simultánea. Esto resulta en tasas de reloj muy altas y en aumento, donde por cada ciclo de reloj son requeridas múltiples instrucciones y datos del sistema de memoria. Se ejerce ası́ una alta presión, también en aumento, sobre el subsistema de memoria caché [15]. Esta presión demanda un muy alto grado de eficiencia en el sistema de memoria caché, siendo necesario continuas mejoras las cuales se basan en nuevos esquemas, estrategias y tecnologı́as. Los distintos parámetros de una memoria caché actúan en forma interrelacionada y compleja sobre la performance de la misma. Pequeños cambios en la estructura de la memoria caché, pueden resultar en sensibles cambios en el desempeño de los sistemas de alta performance [9]. Es por ello que previo a la adopción de un diseño en hardware se realicen primero simulaciones por computadora extensivas. En dichas simulaciones se consideran no solo los aspectos del hardware sino también el software de las aplicaciones que resultan representativas del sistema bajo desarrollo. Un método usual de simulación consiste en la escritura de un programa que simule el comportamiento de la memoria caché propuesta y luego aplicar al simulador una secuencia de referencias a memoria que reproducen la forma en que el procesador real podrı́a ejercitar el diseño considerado. La secuencia de direcciones a memoria es extraı́da de un archivo o conjunto de archivos comúnmente llamados traza y el método de simulación es llamado simulación de memoria caché manejado por trazas [13]. Aunque es conceptualmente simple existen un cierto número de factores que hacen el método de simulación por trazas dificultoso en la práctica. Algunos de estos factores son la dificultad para la colección de la traza, el tipo de información que se colecta y los grandes tamaños de los archivos que contienen la traza [1], [4]. Finalmente el tiempo de simulación para consumir una traza puede resultar muy significativo. Salvados estos inconvenientes, el método de simulación por traza resulta ser muy efectivo y es usado extensivamente en la mayorı́a de los trabajos de investigación sobre memorias caché. C. Trazas de aplicaciones multihilo simultáneo El estudio de una organización de memoria caché optimizada para un ambiente SMT, crea la necesidad de contar con trazas que reflejen el paralelismo real existente entre los hilos, i.e. contar con las múltiples referencias a memoria por ciclo de reloj, llevadas a cabo por los hilos activos en cada fase de ejecución de la aplicación. En la actualidad no se encuentran disponibles públicamente trazas de las mencionadas caracterı́sticas. Más aún, no existen herramientas capaces de generar las trazas requeridas. Tullsen, en uno de sus artı́culos seminales sobre las arquitecturas SMT [12], propone el uso de los benchmarks SPEC [11] para representar los hilos de ejecución. La metodologı́a propuesta por Tullsen puede resultar adecuada para analizar los casos donde las referencias a memoria provienen de hilos totalmente independientes, pero no reflejan la situación de las aplicaciones paralelas reales, con sus distintos grados de concurrencia, datos compartidos y exclusivos para cada hilo, bloqueos y sincronización de hilos, etc. El presente 2 Programa de usuario Instrucciones x86 UCode Módulo (skin) Coregrind VALGRIND Instrucciones x86 UCode (instrumentado) Plataforma de base Fig. 1. Interacción Valgrind-programa usuario. trabajo tiene como principal objetivo realizar una contribución en este sentido, logrando la capacidad de generar trazas de aplicaciones multihilo reales para su posterior uso, principalmente en el estudio de nuevos esquemas de memoria caché. Dicho objetivo es llevado a cabo mediante un conjunto de modificaciones aplicadas a un ambiente de debugging y profiling de dominio público llamado Valgrind [14]. II. EL SISTEMA VALGRIND Valgrind es una herramienta que permite realizar tareas de debugging y profiling sobre programas ejecutables, para ambientes Linux-x86. Básicamente, consiste en una máquina virtual, que implementa un procesador sintético x86, y sobre la cual se ejecutan programas de usuario. Este diseño constituye a Valgrind en una capa adicional, que se inserta entre el programa de usuario y la arquitectura de base (ver figura 1), permitiéndole, entre otras cosas, tener un control total de las referencias a memoria que se efectúan. Cabe aclarar que los programas de usuario que corran sobre Valgrind no necesitan ser recompilados ni revinculados con bibliotecas externas. A. Diseño general El sistema Valgrind está construido en base a un diseño modular, en torno a un núcleo, que realiza el trabajo más pesado. Este módulo central, llamado coregrind, implementa un procesador x86 sintético, e interactúa con otros módulos (skins ó tools), para brindar diferentes funcionalidades (detectar problemas de memoria, verificar la ejecución concurrente de hilos, simular memorias caché, etc.). La interacción de coregrind con los demás módulos se produce de la siguiente manera. Cuando se ejecuta un programa de usuario sobre Valgrind, el módulo central toma el control del mismo, lee el bloque de código a ejecutar y lo pasa al módulo correspondiente. Este módulo, instrumenta [5], [16] el bloque de código recibido, y se lo retorna a coregrind. Posteriormente, el módulo central ejecuta el bloque de código instrumentado. La manera en que un módulo instrumenta el bloque de código provisto por coregrind depende de qué funcionalidad provee dicho módulo. Por ejemplo, el módulo Memcheck (que permite verificar y detectar errores en cada referencia a memoria de un programa de usuario), instrumenta el código agregando sentencias para verificar cada acceso a memoria y cada valor calculado. Los módulos que provee Valgrind son los siguientes: MEMCHECK, ADDRCHECK, HELGRIND, VI SIMPOSIO ARGENTINO DE TECNOLOGIA EN COMPUTACION: AST-PAR002 3 CACHEGRIND, MASSIF, CORECHECK, LACKLEY y NULGRIND. las referencias a memoria hechas por la aplicación, individualizando al hilo de ejecución que la produjo. B. A. Detalles de la arquitectura Valgrind es una biblioteca compartida (valgrind.so), con algunas particularidades, como por ejemplo que tiene prioridad en cuanto a su inicialización. Una vez que esto sucede, toma control completo respecto a la ejecución del programa de usuario, traduciendo las sentencias de código del mismo en otras sentencias nuevas (en formato UCode). Para esto utiliza la función VG (translate), mediante la cual traduce bloques básicos de sentencias. Todo código traducido (e instrumentado) por Valgrind es almacenado en un caché (TC - Translation Cache). Para ejecutar código traducido (y almacenado en el TC), Valgrind utiliza la función VG (dispatch), que posee la lógica necesaria para poder determinar en qué momento ejecutar qué sentencia de código (traducida), ir a buscarla al TC, y alimentarla al procesador real. En cada iteración se obtiene la dirección (real) de la próxima instrucción a ejecutar, dirección que es traducida al rango manejado por Valgrind, para poder recuperar la instrucción desde el TC. Mientras estas instrucciones se encuentren en el TC, Valgrind continúa normalmente la ejecución del programa de usuario. Si una instrucción no se hallara en el TC, Valgrind sale de la función VG (dispatch) para hacer una nueva llamada a VG (translate). C. Microcódigo Valgrind implementa un procesador x86 sintético con instrucciones en un formato propio, conocido como UCode (similar al de un procesador RISC). En el formato UCode, las micro-operaciones se conocen como UInstr. Básicamente, los pasos en los cuales interviene el formato de instrucciones UCode, son los siguientes: 1) Parseo de un bloque básico del programa de usuario en un conjunto de instrucciones UCode. Rutina invocada: VG (disBB) 2) Optimización de las instrucciones UCode obtenidas en el paso anterior. Rutina invocada: vg improve 3) Instrumentación de las instrucciones UCode obtenidas en el paso anterior. Rutina invocada: vg instrument 4) Optimización de las sentencias instrumentadas, para quitar redundancia en los chequeos. Rutina invocada: vg cleanup 5) Asignación de registros. Rutina invocada: vg do register allocation 6) Generación del código final x86. Rutina invocada: VG (emit code) Una instrucción UInstr está conformada por varios campos (de la misma manera que sucede con las microoperaciones). Los más relevantes son el tipo de opcode a ejecutar (en Valgrind, UOpcode), y los valores de los operandos (val1, val2 y val3), entre otros. En particular, los posibles UOpcodes a ejecutar se pueden agrupar de la siguiente manera: GET/PUT, LOAD/STORE, MOV/CMOV y operaciones de la Unidad Aritmético/Lógica (ALU): LEA1/LEA2, CALLM FPU/FPU R/FPU W, JIFZ, INCEIP. III. M ODIFICACIONES APLICADAS A VALGRIND Se presenta aquı́ un conjunto de modificaciones aplicadas a Valgrind (tanto a su módulo central, como también a algunos de sus skins), que permiten recopilar (en tiempo de ejecución) información relacionada al programa de usuario que se está ejecutando. Más precisamente, interesa obtener El módulo a instrumentar De los módulos (o skins) dados en la sección A, los que tienen “contacto” con las referencias a memoria efectuadas por el programa de usuario son: Memcheck y Addrcheck. El primero realiza un análisis completo de la actividad que el programa de usuario presenta respecto al uso de memoria, mientras que el segundo, Addrcheck, es una versión más reducida de Memcheck. Al implementar una funcionalidad más reducida respecto a Memcheck, el módulo Addrcheck permite la ejecución de los programas de usuario en forma dos veces más rápida respecto al primero [10]. Sin embargo, a pesar de su funcionalidad acotada, Addrcheck maneja todos los tipos de instrucciones del programa de usuario, con lo cual, es apto para nuestros fines. Por tales razones citadas, el módulo Addrcheck fue el instrumentado para capturar las referencias a memoria. Según lo detallado en la sección B, todo bloque básico del programa de usuario es traducido a microinstrucciones UCode, mediante sucesivas llamadas a la función VG (translate). Esta funcionalidad es competencia del módulo central (coregrind) ya que, independientemente del skin utilizado, siempre se requiere traducir el programa de usuario a formato UCode. El proceso de traducción de instrucciones de usuario en instrucciones UCode involucra, en uno de sus pasos, la instrumentación de los bloques básicos siendo parseados. Dicha instrumentación trasciende los lı́mites del módulo central, siendo competencia de cada uno de los skins. Ası́, cada skin implementa su propio mecanismo de instrumentación, con el nombre de SK (instrument). Por lo tanto, se modificó la función SK (instrument) del módulo Addrcheck. Para instrumentar un bloque básico, Valgrind inserta en el mismo llamadas a funciones auxiliares o helpers. En el módulo Addrcheck estos helpers, que son invocados cada vez que el flujo de ejecución alcanza ese punto, monitorean todos los accesos de lectura/escritura sobre memoria principal. Por esta razón, para este trabajo se modificó el código de estas funciones auxiliares, con código que permite, entre otras cosas, obtener la dirección de memoria siendo referenciada. B. Consideraciones sobre multihilo En esta sección analizamos el manejo que presenta Valgrind respecto al uso de hilos, y cómo se modificó esa estructura para poder recrear una arquitectura SMT. El sistema Valgrind posee soporte para hilos según el estándar POSIX (pthreads ó POSIX threads). Valgrind reemplaza la biblioteca estándar libpthread.so (que implementa pthreads), por otra propia que encapsula a la primera. En este trabajo analizamos y modificamos el mecanismo de implementación y ejecución de hilos provisto por Valgrind, para que se ajuste a nuestras necesidades. En un ambiente multihilo convencional (uniprocesador, no SMT), el planificador del sistema operativo es el encargado de administrar el uso de CPU por parte de los hilos. Ası́, es quien fracciona el tiempo en rebanadas (o slices), y decide qué hilo debe ejecutarse y por cuánto tiempo, de acuerdo a un esquema de prioridades. En este caso, VI SIMPOSIO ARGENTINO DE TECNOLOGIA EN COMPUTACION: AST-PAR002 si bien a nivel de las aplicaciones la ejecución está paralelizada por el uso de hilos, está claro que a nivel del procesador la ejecución se serializa. Valgrind presenta un esquema similar, con un planificador (vg scheduler), encargado de ordenar la ejecución de los hilos del programa de usuario. Para ello, en lugar de contemplar hilos con prioridades, vg scheduler ejecuta una cantidad fija de instrucciones por hilo, antes de efectuar el cambio y usa una polı́tica round-robin, ejecutando 50000 bloques básicos por hilo [10]. Como se señaló en la sección A, en una arquitectura SMT, todos los hilos están disponibles para ser ejecutados en cada ciclo de reloj. Eventualmente, algunos de ellos podrı́an estar bloqueados porque, por ejemplo, realizaron una operación de E/S o tuvieron un desacierto en la memoria caché. Teniendo en cuenta que Valgrind realiza un cambio de contexto cada 50000 bloques básicos ejecutados, implica un modelo de ejecución completamente alejado del modelo SMT. Para resolver esta situación, se modificó el planificador de Valgrind para que cambie de contexto por cada bloque básico ejecutado. Cada bloque básico equivale a muy pocas instrucciones x86, con lo que se logra obtener en la práctica solo una referencia a dato en memoria por cada conmutación de hilo. De esta manera la secuencia de referencias a memoria generada por la herramienta, refleja el paralelismo a nivel de hilos presente en la aplicación, aspecto de fundamental importancia para la simulación de memorias cache en ambientes SMT. Se define aquı́ la forma para la salida del flujo de referencias a memoria. Todas las referencias capturadas son escritas en un archivo, con un formato simple, que contiene los siguientes campos: Id del hilo Dirección de memoria Los siguientes son ejemplos de referencias a memoria capturadas a partir de las modificaciones efectuadas a Valgrind para una aplicación con cuatro hilos de ejecución: [ [ [ [ 1] 2] 3] 4] [ [ [ [ ... 2617240328] 1963012180] 1964569408] 1967722008] ... Finalmente dado los enormes tamaños de los archivos de trazas tı́picos, los mismos se generan y comprimen “al vuelo”, para lo cual se utiliza el algoritmo LZ77, mediante la biblioteca zlib. IV. C ONCLUSIONES Se presentó un ambiente para la recolección de referencias a memoria de programas multihilo. La herramienta utiliza directamente el código ejecutable de la aplicación analizada, con lo cual no es imprescindible contar con los códigos fuente de la misma. Esto último es de gran importancia dado que es posible obtener las trazas de aplicaciones comerciales, de las cuales usualmente no se dispone de los códigos fuente mencionados. La fina granularidad lograda en la captura de las referencias, hacen a las trazas generadas con la herramienta propuesta ideales para la simulación de un esquema SMT. Esto es de gran importancia, dado que no 4 existen herramientas libres y de código abierto que permitan realizar la captura de trazas de programas multihilo que se ejecutan en arquitecturas SMT. Asimismo el esquema SMT ha adquirido gran importancia, y es usado en procesadores comerciales como Intel Pentium IV HT. También Intel, por ejemplo, lanzó su último procesador, el cual posee un doble núcleo (Dual-Core) que incorpora la arquitectura SMT. Por su parte, IBM y Sun Microsystems también están implementando el esquema SMT en productos como los servidores IBM Power5 y UltraSparc IV, respectivamente. Esto indica que el esquema SMT promete seguir siendo explotado en los próximos años y de ahı́ la importancia de poder contar con los ambientes de simulación adecuados para estas arquitecturas. V. AGRADECIMIENTOS Queremos agradecer a Leandro Santi y Alejandro Gramajo de la Facultad de Ingenierı́a, U.B.A., por los valiosos aportes realizados al presente trabajo. También agradecemos a Julian Seward, Nick Nethercote y Jeremy Fitzhardinge, de KDE y creadores de Valgrind, quienes aportaron sus ideas y consejos respecto a cómo modificar dicha herramienta para la captura de referencias a memoria. Finalmente agradecemos a Henry Levy, de la Universidad de Washington y uno de los creadores de la arquitectura SMT, quién aportó información sobre esta nueva tecnologı́a. El presente trabajo cuenta con subsidios de la Universidad de Buenos Aires y el Consejo Nacional de Investigaciones Cientı́ficas y Técnicas (CONICET). R EFERENCIAS [1] A. Agarwal, L. Sites y M. Horowitz, “ATUM: a new technique for capturing address traces using microcode”, Proceedings of the 13th annual international symposium on Computer architecture, Tokyo, Japón, pp. 119-127, 1986 [2] S. Eggers, J. Emer, H. Levy, J. Lo, R. Stamm, D. Tullsen; Simultaneous Multithreading: A Platform for Next-Generation Processors; IEEE Micro; pp. 12-19; 1997. [3] Intel Corporation, http://www.intel.com. [4] J. Larus y T. Ball, “Optimally Profiling and Tracing Programs”, Technical Report 1031 - Computer Sciences Department - University of Wisconsin-Madison (Madison), 1991. [5] J. Larus y T. Ball, “Rewriting Executable Files to Mesure Program Behavior”, Technical Report 1083 - Computer Sciences Department - University of Wisconsin-Madison (Madison), 1992. [6] D. A. Patterson y J. L. Hennessy, Computer Architecture. A Quantitative Approach, 1ra edición, Morgan Kaufmann Publishers, 1990. [7] D. A. Patterson y J. L. Hennessy, Computer Architecture, A Quantitative Approach, 2da edición, San Mateo, California: Morgan Kaufmann Publishers, 1995. [8] D. A. Patterson y J. L. Hennessy, Computer Architecture. A Quantitative Approach, 3ra edición, Morgan Kaufmann Publishers, 2000. [9] S. Przybylski, “Cache and Memory Hierarchy Design. A Performance Directed Approach”, Morgan Kaufman Publishers, 1990. [10] J. Seward, N. Nethercote; Valgrind, version 2.1.0; Manual correspondiente a la versión 2.1.0 del sistema Valgrind; 2004. [11] Standard Performance Evaluation Corporation, http://www.spec.org. [12] D. Tullsen, S. Eggers, H. Levy; Simultaneous Multithreading: Maximizing On-Chip Parallelism; Proceedings of the 22nd Annual International Symposium in Computer Architecture, 1995. [13] R. A. Uhlig y T. N. Mudge, “Trace-Driven Memory Simulation: A Survey”, ACM Computing surveys, vol. 29, No. 2, pp. 128-170, June 1997. [14] J. Seward, N. Nethercote, J. Fitzhardinge; Valgrind version 2.0.0; http://valgrind.kde.org/index.html. [15] A. Wulf y S. A. McKee, “Hitting the Memory Wall: Implications of the Obvious”, ACM Computer Architecture News, vol. 23, nro. 1, pp. 20-24, 1995. [16] N. Nethercote; Dynamic Binary Analysis and Instrumentation; Dissertation submitted for the degree of Doctor of Philosophy at the University of Cambridge; 2004.