Técnicas de asignación de registros en un compilador
Transcripción
Técnicas de asignación de registros en un compilador
Técnicas de asignación de registros en un compilador Primera Parte Esteban Meneses y Francisco Torres [email protected] [email protected] Se presenta la descripción del teorema de los cuatro colores, cuestionante que generó muchas discusiones en matemáticas y que impulsó algunas técnicas de coloreo de mapas. Además, se hace un recorrido por el algoritmo de Gregory Chaitin para la asignación de registros por medio del coloreo de grafos. Esta técnica resulta bastante sencilla y muy poderosa a la vez, pues logra integrar la asignación de registros y el vaciado de los mismos en un solo proceso. Palabras clave: compiladores, optimización, asignación de registros, vaciado de registros, coloreo de grafos, grafos de interferencia. ara representar los problemas y las soluciones a los mismos, los programadores utilizan los lenguajes de programación. Una herramienta indispensable, sin lugar a dudas. Sin embargo, para poder aprovechar el poder de los lenguajes, se necesita un chasis que soporte la velocidad de los mismos. Esta estructura, que subyace en cualquier lenguaje, la conforman los compiladores y los intérpretes. Cualquiera de estos dos programas tiene una función bien definida: traducir un código de un lenguaje a otro. La mayor parte de los casos, la compilación traduce un lenguaje de alto nivel (el fuente o front-end) a otro de un nivel menor (el destino o backend, que probablemente es el lenguaje de una máquina, virtual o física). El compilador es entonces, un fantasma necesario en la mansión de los lenguajes. Pero el diseño de este actor silencioso requiere mucho trabajo. De hecho, especificar y desarrollar un lenguaje es en realidad construir todo un motor de compilación mediante el cual este lenguaje tendrá utilidad en alguna máquina [1]. Paralelo al concepto de utilidad está presente otro de igual importancia: eficiencia. En su manera más sencilla, la eficiencia es la mejoría del tiempo de un trabajo sin que empeore su calidad. El código generado por un compilador debe ser eficiente, para que los usuarios del sistema sean igualmente eficientes. Una manera de incrementar la eficiencia del código generado es mejorando muchos de los procesos que están insertos dentro de todo el programa. Estas mejorías se llaman optimizaciones (aunque el término sea discutible). Existe una gran cantidad de tales optimizaciones [6], entre ellas: eliminación de operaciones innecesarias (como eliminación de sub-expresiones comunes, identificación de código inalcanzable, optimización de saltos), determinación de operaciones costosas (uso de instrucciones menos costosas, operación y propagación de constantes, sustitución de código), calendarización de instrucciones (que es un problema NP-completo [2]), 1 asignación y vaciado de registros . La asignación de registros juega un papel preponderante en toda la familia de mejorías al código generado por un compilador. P ¿Qué relación hay entre un mapa de Inglaterra que debía ser coloreado en 1852 y técnicas modernas de asignación de registros en un compilador? 1 Abril 2001 Los términos en inglés para estos procesos son register allocation y register spilling, respectivamente. 9 La traducción que hace un compilador suele no llevarse a cabo en un solo paso. De esta forma, el código fuente es traducido a una representación intermedia (IR), parecida a los byte-code de Java o a los P-code de Visual Basic. En este pseudo-lenguaje se puede suponer que hay un número infinito de registros. Así pues, en pasos siguientes el compilador toma ese código en IR y lo traduce a un lenguaje que corresponda con alguna arquitectura particular, con un número finito de registros. Y aquí está el dilema, finito o no finito. El cambio en la cardinalidad del conjunto de registros de los lenguajes destino e intermedio es lo que da vida al problema del cual se va a tratar en este artículo: asignación y vaciado de registros. Ahora, ¿por qué preocuparse tanto por el buen uso de los registros? ¿acaso en las máquinas de hoy no existe suficiente memoria? La respuesta a estas preguntas está en las dos características del conjunto de registros. Este conjunto, primero que todo, es pequeño (no se tiene infinita cantidad de los mismos) y además los registros son rápidos, más que la memoria. De manera que los registros son uno de los recursos críticos de las arquitecturas computacionales. Mientras desarrollaba el primer compilador de FORTRAN, John Backus sugirió una idea, que al traducirla al diseño de ahora, pudiera decirse así: “Durante la optimización, imagine que tiene un conjunto infinito de registros; trate la asignación de registros como un problema separado” [4]. Sin embargo, como toda arquitectura tiene un número determinado de registros, entonces se debe usar la asignación de registros. Esta actividad, puede ser desarrollada en varios niveles: sobre expresiones, en bloques básicos completos (secuencias de instrucciones donde no existen ni etiquetas ni saltos a otras posiciones), sobre rutinas enteras o entre una colección de rutinas. Cuando se habla de la asignación de registros, se está tratando con los rangos de vida. Por ejemplo, en la expresión X=Y+1, tenemos una defini10 ción de X y un uso de Y. Un rango de vida de una variable empieza cuando ésta es definida y recorre todos los usos de la misma hasta el último de ellos. Desafortunadamente, la asignación de registros es difícil. Los detalles idiosincráticos de la máquina complican aún los asignadores más simples. Empero, se han dado muchas estrategias para resolver el problema. Una de ellas es el coloreo de grafos, que ofrece una abstracción bastante simple. Construyendo un grafo, que representa la interferencia entre los rangos de vida de las variables, se puede establecer un algoritmo para el mapeo de registros. El coloreo de grafos ha tenido una buena aceptación entre los implementadores de compiladores. Pero también otros autores[8] piensan que esta técnica es un poco ambiciosa, en el sentido de que trata de resolver un problema que no es indispensable para el compilador (es decir, es una optimización más). El creador de la estrategia de coloreo de grafos para resolver el problema de la asignación de registros es Gregory Chaitin, quien fundó toda una escuela de seguidores que fueron mejorando su propuesta y que generaron todo un abanico de posibilidades para este problema. La técnica de Chaitin se basa en el coloreo de grafos que pudo estar vinculado con la resolución de un problema matemático muy importante que fue resuelto por esas fechas [3][7]. En el resto del artículo, se supondrá que el asignador trabaja en un código intermedio de bajo nivel, parecido a un ensamblador. Antes del asignador, el código intermedio puede referenciar un número ilimitado de registros (registros virtuales o simbólicos), que en adelante se llamarán simplemente variables. El objetivo de la asignación es reescribir el código intermedio, de manera que se usen sólo los registros que están disponibles en la máquina de destino (los registros reales, físicos o de la máquina). En las siguientes páginas se hará una emocionante cirugía al fantasma de los compiladores. No hay por qué temer, la operación está llena de acotaciones sencillas pero bastante ingeniosas. El problema de los cuatro colores Uno de los antecedentes que pudo tener la propuesta de Gregory Chaitin es la resolución de uno de los problemas matemáticos más famosos. Existen muchos de estos problemas que toman años en ser resueltos. Quizás el más popular de ellos es el Último Teorema de Fermat, probado hace algunos años por el matemático inglés Andrew J. Wiles de la Universidad de Princeton. Otro problema, no tan famoso es el de los cuatro colores. Planteado hace aproximadamente 125 años, el problema de los cuatro colores despertó curiosidad en casi todos los matemáticos, haciendo que se le dedicarán tesis y meses completos de trabajos para tratar de resolverlo. El enunciado [3], muy simple por cierto, estipula lo siguiente: “Cuatro colores son suficientes para colorear un mapa dibujado en un plano o una esfera, de manera que no existan dos regiones que sean vecinas (comparten una frontera) y que estén pintadas del mismo color”. La historia de este problema incluye a muchos grandes matemáticos, de la altura de Augustus DeMorgan (el de las leyecitas), Sir William Rowan Hamilton, Arthur Cayley y August Ferdinand Möbius. Todo empezó cuando con un hombre llamado Francis Guthrie, hizo el descubrimiento mientras coloreaba regiones en un mapa de Inglaterra, alrededor de Octubre de 1852. Tiempo después se lo comentó a su hermano Frederick y éste le dijo a su profesor de matemáticas, DeMorgan. Este último contactó a Hamilton y de allí en adelante la fama se extendió por todo el mundo matemático. DeMorgan suponía (erradamente) que bastaba demostrar, para probar la conjetura de los cuatro colores, que no se podía construir un mapa en donde se tengan cinco países, tales que cada uno com- Tiempo Compartido parta una frontera con los otros cuatro. Pero aún probando esto, no queda claro si de allí se puede seguir la demostración de la conjetura de los cuatro colores. Por cierto, Möbius se divertía asignándole este problema a sus alumnos, sabiendo que no era posible construir un mapa con esas características. Los primeros intentos no esperaron mucho y la primera “prueba” fue publicada por un joven matemático, llamado A.B. Kempe. La prueba de este señor estuvo en boga por 11 años, hasta que otro matemático, Heawood, descubrió un error en ella. Aunque Kempe era un matemático amateur, su solución no es amateur, pues él introdujo técnicas que todavía se utilizan en la actualidad para el coloreo de mapas. Por otro lado, el mismo Heawood modificó la pseudoprueba de Kempe para demostrar que cinco colores son suficientes para colorear un mapa. Para no aburrir a nadie, en el verano de 1976, Kenneth Appel y Wolfgang Haken de la Universidad de Illinois anunciaron que habían resuelto el problema de los cuatro colores. Las sofisticadas técnicas que usaron Appel y Haken parecen haber tenido éxito en la prueba del problema de los cuatro colores. Sin embargo, su prueba causó toda una ola de polémica, dado que su prueba incluye un análisis facilitado por computadora de 1936 casos especiales y esta parte requirió varios años de revisión.[3] Además de aclarar el panorama, con la generación de ejemplos y ciertos datos estadísticos, las computadoras pueden hacer un trabajo realmente brutal, como el que hizo la máquina usada por Appel y Haken, la que tuvo que realizar, entre otras cosas, 1010 operaciones separadas en sus circuitos de alta velocidad y más de 1200 horas de cálculos. Algo importante de mencionar es que todas las técnicas que se utilizan para colorear mapas se pueden adaptar para colorear grafos planos y estas últimas técnicas sirven de base para generar algoritmos de coloreo para grafos más generales. Si el lector quiere profundizar en este tema, puede consultar [7]. Abril 2001 Cabe resaltar que el problema de colorear un grafo (así como el de asignación de registros) también es NPcompleto, o sea, no se conoce un algoritmo que en tiempo polinomial pueda resolver el problema de forma óptima. Sin embargo existen varias técnicas heurísticas que pueden dar una solución aceptable para este desafío. Para los lectores incrédulos, se presentará a continuación el 4-coloración (o sea, con 4 colores) de una sección de Europa. Nótense dos cosas. Primero, no existen países vecinos que tengan el mismo color. Segundo, el “mar” o todo lo que rodea los países también debe tener un color asignado (gris claro). Aquellos que quieran impresionar a sus amigos tal vez pueden presentarles una 4-coloración del mapa de los cantones de nuestro país. por medio del coloreo de grafos. ¿Tienen relación esos dos temas? Claro que sí, y ahí reside precisamente la belleza de esta propuesta. En general, la idea es colorear un grafo de dependencias de los rangos de vida de las variables que aparecen en la representación intermedia (IR). Los rangos de vida se refieren a los lapsos en que está viva una variable. Por ejemplo, en un programa en IR, la variable X (que en este caso sería un registro simbólico) se define en la quinta instrucción: X=6+Y. Luego, la variable X es usada varias veces, hasta que en la vigésima instrucción se utiliza por última vez: Z=X–2. Entonces todo ese lapso de la quinta a la vigésima instrucción es el rango de vida de X, porque X en ese lapso tiene sentido para el programa, ¡está viva! Después del último uso de X, ésta pierde la importancia y por ende no tiene caso considerarla más. Ahora, si dos variables interfieren, es decir hay una instrucción donde ambas están vivas, no pueden ser asignadas al mismo registro real porque esto claramente entorpecería la corrida del Figura 1: 4-coloración de una sección de Europa programa. Entonces, el primer paso que se debe seguir en el procesamiento de un programa escrito La propuesta de Chaitin en IR, debe ser el de usar las bien Las ideas de Gregory Chaitin forman la conocidas técnicas de análisis de flujo columna vertebral de muchas investi- de datos [5]. Este análisis consiste en gaciones que se han hecho al respecto saber cuando empieza y cuando terde la asignación de registros. La gran mina cada uno de los rangos de vida de virtud de su propuesta es que logra variables en IR. El paso que sigue es la construcción integrar el problema de la asignación y del grafo de interferencia G. Los nodos el vaciado de registros en un algoritmo en G representan los rangos de vida de sencillo.[4] Chaitin y su equipo desalas variables y los arcos representan rrollaron el asignador como parte de un interferencia. Así, hay un arco en G del compilador para PL8 en el laboratorio nodo i al nodo j si y sólo si los rangos de investigación de IBM en Yorktown de vida que representan los nodos Heights, NY. Presentaron sus ideas en interfieren; esto es, están vivos en el los inicios de los ochentas. mismo momento y por ende no pueden Como se adelantó, el algoritmo de Chaitin consiste en asignar registros ocupar el mismo registro físico. Los 11 (a) (b) (c) (d) ria (por ello aparece coloreado con rayas). El asignador de Yorktown Figura 2: Grafos de inferencia rangos de vida que interfieren con un rango de vida, se llaman los vecinos de él; el número de vecinos de un nodo en el grafo es llamado el grado de ese nodo. Intuitivamente, se dice que dos rangos de vida interfieren, si al ser asignados esos dos rangos al mismo registro físico, el programa cambia de sentido. Entonces, tenemos hasta este momento un número de rangos de vida de variables (que no tiene cota) y un número finito de registros de la máquina (supóngase que se tienen k). Para encontrar una asignación de registros para el programa, el algoritmo busca una k-coloración del grafo de interferencia. Como encontrar esta coloración es un problema NP-completo, el compilador utiliza una técnica heurística para encontrar esa coloración; no está garantizado que el algoritmo encuentre una k-coloración para un grafo k-coloreable. Si no se puede encontrar una k-coloración, algunos rangos de vida son víctimas de un vaciado, o sea, que se mantienen en memoria en lugar de registros. Y aquí está la analogía más interesante. Asignar registros es: colorear grafos. De esta forma habrá tantos nodos en el grafo como rangos de vida en la rutina y tantos colores como registros físicos de la máquina. Y colorear el grafo significa asignar registros reales a variables. A manera de ejemplo, supóngase que se analizó un programa y se construyó el grafo de interferencia, que se observa en la figura 2(a). Resulta que la arquitectura en la que se correrá 12 ese programa tiene 3 registros reales. De esta forma se debe colorear el grafo con 3 colores (negro, blanco y gris; uno por cada registro físico). Se pintará el grafo siguiendo de una manera intuitiva el algoritmo de Chaitin. Primero que nada, se debe encontrar un nodo con menos de 3 vecinos. Aparecen dos opciones: G y H. Se escoge uno de ellos, G. Luego se introduce en una pila y se elimina del grafo al nodo y a todos los arcos incidentes en él. Seguidamente, se vuelve a buscar un nodo con menos de 3 vecinos, solo hay una opción H. Se elimina de igual manera, quedando la figura 2(b). Ahora, se llegó a un punto donde todos los nodos tienen al menos 3 vecinos. En este caso se debe buscar uno de ellos para guardarlo en memoria. Se escoge C arbitrariamente y este nodo no se coloca en la pila. Así se obtiene la figura 2(c). De allí en adelante es sencillo seguir seleccionando nodos y borrando elementos del grafo. Supóngase que el resto de los nodos se eliminó en el siguiente orden D, A, B, E, F. Cuando ya no hay más nodos que eliminar del grafo, se procede a colorear el grafo original, sacando los nodos de la pila. Se toma el primer nodo en la pila, F en este caso, y se le asigna un color, por ejemplo negro. Después, se saca E y se le asigna un color distinto al de sus vecinos, que puede ser blanco. Así se prosigue sucesivamente hasta que el grafo original queda totalmente coloreado, como en la figura 2(d). En ella se puede notar que C no tiene ningún color de los iniciales. Esto se debe a que fue víctima de un vaciado a memo- Más formalmente, el asignador de Yorktown (elaborado por Chaitin y compañía, [4]) tiene varias fases, que se pueden observar en la figura 3: = Renumerar: encuentra todos los rangos de vida en una rutina y les asigna un número único a cada uno. = Construir: se construye el grafo de interferencia. 2 = Fundir : el asignador remueve las copias innecesarias, eliminando las instrucciones de copia (estas instrucciones son las que tienen la forma x=y) y combinando los rangos de vida del fuente y del destino. Por ejemplo, supongamos que se tiene un rango de vida de la variable x, luego el último uso de ésta es una copia (y=x), de manera que todas las apariciones de y se pueden sustituir por x. Una copia puede ser removida, si los rangos de fuente y de destino no interfieren. = Costo del spill: en la preparación para el coloreo, un costo de spill es estimado para cada rango de vida. = Simplificar: esta fase, junto con la de seleccionar, ayuda a colorear el grafo. La simplificación examina repetidamente los nodos en el grafo G, removiendo aquellos nodos con grado menor que k. Conforme cada nodo es removido, sus arcos también se quitan (decrementando el grado de sus vecinos) y es empujado en el tope de una pila. = Seleccionar: los colores son escogidos en el orden determinado por la simplificación. Por turnos, cada nodo es sacado de la pila, reinsertado en el grafo G, y se le da un color distinto al de sus vecinos. El significado de la línea punteada se discutirá en la segunda parte de esta entrega. = Código de spill: este código es insertado para cada rango de vida que debe ser víctima de un spill. 2 La palabra original en inglés para este proceso es register coalescing. Tiempo Compartido vaciado de registros. Sin embargo, descuida algunos aspectos técnicos que pueden ocasionar perjuicios en el rendimiento. Algunas mejoras sobre este algoritmo serán analizadas en la segunda parte de este artículo. De manera que, el lector no debe perderse la Figura 3: Pasos del asignador de Yorktown siguiente cirugía al compilador: un paciente ciertamente El corazón del asignador de (porque éste elimina la mayor cantidad de difícil de dar de alta. Yorktown es el algoritmo de coloreo. enlaces). Sin embargo existen varias La técnica heurística desarrollada por fórmulas para determinar la víctima. Una Chaitin para el coloreo de grafos, tiene de ellas, también propuesta por Chaitin, Referencias una eficiencia de O(n+e), donde n es el es vaciar el nodo que tenga la menor [1] Aho, Alfred et al. Compiladores: principios, técnicas y herramientas. México: número de rangos de vida a ser razón de costo de spill sobre grado: Addison Weley Iberoamericana, 1985. coloreados y e es el número de arcos en Mn = Coston / Gradon [2] Appel, Andrew W. Compiling with el grafo de interferencia. Chaitin de- Donde el costo del nodo n se calcula de continuations. New York, Estados Unidos: mostró que su procedimiento puede ser acuerdo a ciertos parámetros de la Cambridge University Press, 1992. usado para lograr tanto un coloreo del máquina. Una vez que un nodo (que [3] Barnette, David. Poly-hedra and the grafo como un vaciado de registros de representa un rango de vida L) ha sido fourcolor problem. Estados Unidos: AMS, 1983. una manera integrada. escogido para ser vaciado a memoria, Ahora, ¿cómo trabaja este algoritmo? se debe insertar una grabación hacia [4] Briggs, Preston. “Register allocation via Graph Coloring”. Ph.D. Disertation, HousBien, la fase de simplificación repetida- memoria después de cada definición de ton, Texas: Rice University, 1992: Sitio: csmente remueve nodos del grafo y los L y una carga antes de cada uso de L. tr.cs.cornell.edu. empuja en una pila. En la selección, los La condición de que un nodo deba tener [5] Chaitin, Gregory J. “Register allocation nodos son sacados de la pila y menos de k vecinos para ser coloreado and spilling via graph coloring”. ACM: devueltos al grafo. Un nodo es remo- es un muy restrictiva y eso ayuda a que SIGPLAN’82 Symposium on Compiler Construction.Volúmen17(6):98-105,jun. 1982 vido del grafo a la pila, sólo si su grado el algoritmo encuentre menos frecuen[6] Fischer, Charles y Richard LeBlanc Jr. es menor que k (donde k es el número temente una coloración óptima. Por Crafting a Compiler with C. Redwood City, de registros de la máquina). Así pues, ejemplo, el grafo de la figura 2 puede CA, Estados Unidos: The Benjamin/Cummings cuando la selección mueve un nodo N ser coloreado totalmente con 3 colores Publishing Co., 1991. desde la pila al grafo, N tendrá así (asígnele a C el color negro), sin [7] Faaty, Thomas L. y Paul C Kainen. The menos de k vecinos y claramente habrá necesidad de utilizar el vaciado de four-color problem: assaults and conquest. New York, Estados Unidos: Mc-Graw Hill un color disponible para N en el grafo. registros. A la par de este problema 1977 La etapa de simplificación sólo existen muchos otros que tratarán de [8] Fraser, Christopher y David Hanson. A remueve un nodo cuando se puede pro- ser cicatrizados en la próxima operaretargetable C compiler: design and bar que al nodo se le podrá asignar un ción a los compiladores, que se hará en implementation. Estados Unidos: Benjacolor en el grafo actual. Conforme cada la segunda parte de este artículo. min/Cummings Publishing Company, 1995. rango de vida (o nodo) se remueve del grafo, el grado de sus vecinos se decreConclusiones T Esteban Meneses es estudiante avanmenta. Así, se va probando, en turnos, zado de la carrera de Ingeniería en que es posible asignarles un color a La asignación de registros es una de las Computación del I.T.C.R. Actualmencada uno de ellos. En la etapa de selec- optimizaciones más importantes que te realiza su práctica de especialidad en ción a los nodos se les asigna un color pueden hacerse al código generado por ArtInSoft. en orden inverso al cual fueron remo- un compilador. Es un problema NPvidos. completo y por ello se han propuesto T Francisco Torres es bachiller en Cabe destacar, que de aquellos nodos muchas alternativas heurísticas para Informática de la U.C.R., Máster en que no se hayan podido remover del resolverlo de una manera eficiente. El Ciencias de la Computación de Georgia grafo a la pila, algunos necesitan ser algoritmo propuesto por Chaitin y su Tech y Doctor en Ciencias de la Comvaciados a memoria. ¿Cómo escoger el equipo de investigación fue una de las putación del Georgia Tech. Actualmennodo a vaciar en memoria? Chaitin primeras técnicas serias en este campo. te labora como profesor e investigador propuso un método muy sencillo: esco- Tiene la ventaja de ser muy sencillo y del Instituto Tecnológico de Costa ger el nodo que tenga más vecinos de abstraer también el problema del Rica. Abril 2001 13