Los e-graphs (equality graphs) han demostrado ser una herramienta poderosa para la optimización de programas y la reescritura de términos, al representar conjuntos de términos equivalentes en una estructura compacta y permitir la aplicación eficiente de reglas de reescritura. Sin embargo, muchas optimizaciones en compiladores no son meras equivalencias, sino 'refinamientos': transformaciones que mueven un programa de una especificación más abstracta o ambigua a una más concreta y ejecutable, sin perder corrección. Por ejemplo, la resolución de 'don't care conditions' en el diseño de circuitos digitales o la especificación de comportamientos ante desbordamientos de enteros.
El problema fundamental que los refinement e-graphs buscan resolver es cómo integrar esta noción de orden parcial (<=) directamente en la estructura de datos y los algoritmos de los e-graphs, de manera que sea tan 'baked-in' como la igualdad. Esto permite que las optimizaciones que son unidireccionales por naturaleza sean modeladas y aplicadas de forma más natural y eficiente, en lugar de ser codificadas de forma ad-hoc o como equivalencias forzadas que podrían limitar el espacio de optimización o introducir errores semánticos.
Arquitectura del Sistema
La arquitectura de un refinement e-graph se construye sobre la base de un e-graph tradicional, que combina un Union-Find para gestionar las clases de equivalencia (e-classes) y un hash consing para la deduplicación de nodos (enodes). La innovación principal es la sustitución del Union-Find estándar por un 'Inequality Union-Find'. Este componente no solo gestiona la igualdad, sino que también mantiene conjuntos de 'uppers' y 'lowers' para cada e-class, representando las relaciones 'mayor o igual que' (>=) y 'menor o igual que' (<=) respectivamente. Estas relaciones se propagan transitivamente y colapsan ciclos a igualdad.
Los algoritmos clave de los e-graphs —congruence closure, e-matching y extracción— se adaptan para manejar la relación de orden. La 'refinement closure' reemplaza la congruence closure, aplicando reglas de inferencia que respetan la monotonicidad de las funciones. Por ejemplo, si a <= b y c >= d, y una función diff es monótona en su primer argumento y anti-monótona en el segundo, entonces diff(a,c) <= diff(b,d). Para esto, cada símbolo de función se anota con una 'variance signature' (MONO, ANTI, UNKNOWN) para cada uno de sus argumentos. El e-matching y la extracción se parametrizan con un 'Mode' (LE, EQ, GE) que indica la dirección de la búsqueda, y este modo se ajusta según la varianza de los argumentos al descender por el árbol de términos. La extracción, por ejemplo, puede buscar el término 'más refinado' (más concreto) o el 'menos refinado' (más abstracto) que satisfaga una relación dada.
Flujo de Refinement Closure
- 1 Iterar Enodes Para cada nodo (f, children) en el e-graph.
- 2 Obtener Varianza Determinar la varianza (MONO, ANTI, UNKNOWN) de cada argumento de 'f'.
- 3 Expandir Hijos Para cada hijo, expandir su e-class según el modo de varianza (LE, GE, EQ).
- 4 Generar Combinaciones Crear nuevas combinaciones de hijos expandidos (producto cartesiano).
- 5 Añadir Relación LE Para cada combinación, añadir una relación 'le' entre el nodo original y el n...
| Capa | Tecnología | Justificación |
|---|---|---|
| data-processing | E-Graphs (Equality Graphs) | Estructura de datos fundamental para representar y optimizar programas mediante la gestión de equivalencias y refinamientos de términos. Permite aplicar reglas de reescritura de forma eficiente. vs Term Rewriting Systems (sin e-graphs), Abstract Interpretation (para análisis de programas) |
| data-processing | Union-Find Data Structure | Componente central para gestionar las clases de equivalencia (e-classes) de términos. En este caso, una variante 'Inequality Union-Find' que también rastrea relaciones de orden parcial (<=). vs Disjoint Set Union (DSU) sin extensiones de desigualdad |
| data-processing | Hash Consing | Mecanismo para la deduplicación de nodos (enodes) y términos, asegurando que cada término único tenga una única representación en memoria, lo que es crucial para la eficiencia de los e-graphs. vs Hashing estándar sin deduplicación global |
| data-processing | Python (prototipo) | Lenguaje de implementación para el prototipo 'microegg' y 'refinement-microegg', utilizado para demostrar los conceptos y algoritmos. vs Rust (para rendimiento en sistemas de compilación), C++ |
Trade-offs
Ganancias
- ▲ Expresividad en optimizaciones de compiladores
- ▲ Modelado más natural de refinamientos unidireccionales
- ▲ Cohesión conceptual al integrar <= directamente
Costes
- △ Complejidad de implementación y rendimiento
- ▲ Comportamiento menos predecible de la 'refinement closure' (posible explosión de nodos)
- △ Menor rendimiento en comparación con la igualdad pura
class LE_EGraph():
parents : list[Id] = field(default_factory=list)
memo : dict[Node, Id] = field(default_factory=dict)
uppers : list[set[Id]] = field(default_factory=list)
lowers : list[set[Id]] = field(default_factory=list)
variance : dict[str, tuple[Variance, ...]] = field(default_factory=dict)
def find(self, a : Id) -> Id:
while self.parents[a] != a:
a = self.parents[a]
return a
def union(self, a : Id, b : Id) -> None:
a,b = self.find(a), self.find(b)
if a == b:
return
self.parents[b] = a
self.uppers[a] |= self.uppers[b]
self.lowers[a] |= self.lowers[b]
def add_le(self, a : Id, b : Id) -> None:
a,b = self.find(a), self.find(b)
if a == b:
return
self.uppers[a].add(b)
self.lowers[b].add(a)
def all_le(self, a : Id) -> set[Id]:
a = self.find(a)
u = {self.find(x) for x in self.uppers[a]}
u.add(a)
todo = list(u)
while todo:
x = self.find(todo.pop())
for y in self.uppers[x]:
y = self.find(y)
if y not in u:
u.add(y)
todo.append(y)
return uclass Mode(Enum):
LE = -1
EQ = 0
GE = 1
class Variance(Enum):
MONO = 1
ANTI = -1
UNKNOWN = 0
def act(self, mode: Mode) -> Mode:
match self:
case Variance.MONO:
return mode
case Variance.ANTI:
return Mode(-mode.value)
case Variance.UNKNOWN:
return Mode.EQFundamentos Teóricos
La noción de e-graphs tiene sus raíces en el trabajo de Robert Tarjan sobre el algoritmo de Union-Find para la detección de ciclos y la gestión de conjuntos disjuntos, y en el concepto de 'congruence closure' desarrollado por Nelson y Oppen en la década de 1970 para la decisión de la satisfacibilidad de teorías combinadas. La extensión a 'refinement' se conecta con la teoría de la reescritura de términos y los sistemas de tipos con subtipos, donde las relaciones de orden parcial son fundamentales.
Conceptos como la monotonicidad y la contravarianza de funciones en relación con un orden parcial son bien conocidos en la lógica y la teoría de tipos, especialmente en el contexto de la inferencia de tipos y la programación funcional. La idea de 'don't care conditions' se remonta a los fundamentos del diseño de circuitos digitales y la minimización de funciones booleanas, donde se utilizan para simplificar expresiones lógicas y optimizar la implementación de hardware. La integración de estas ideas en una estructura de e-graph proporciona un marco unificado para aplicar principios de optimización que históricamente han sido tratados de forma separada.