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

Documentos relacionados