5

¿Alguien sabe de alguna discusión de documentos algoritmos de alineación? Y muy relacionado, la relación de gráfico padre-hijo para llamar al gráfico.Algoritmo de alineación

Antecedentes: tengo un compilador escrito en Ocaml cuales inlines agresiva funciones, principalmente como resultado de esto y algunas otras optimizaciones que genera código más rápido para mi lenguaje de programación que la mayoría de los demás en muchas circunstancias (incluyendo incluso C).

Problema n. ° 1: El algoritmo tiene problemas con la recursión. Para esto, mi regla es solo para poner en línea a los niños en los padres, para evitar la recursión infinita, pero esto impide que las funciones hermanas se alineen una con la otra.

Problema n. ° 2: No conozco una forma sencilla de optimizar las operaciones de alineación. Mi algoritmo es imprescindible con la representación mutable de los cuerpos de función porque no parece remotamente posible hacer un algoritmo de alineación funcional eficiente. Si el gráfico de llamadas es un árbol, es claro que una alineación ascendente es óptima.

Información técnica: La alineación consiste en una serie de pasos de alineación. El problema es ordenar los pasos.

Cada paso funciona de la siguiente manera:

  • hacemos una copia de la función a ser inline y beta- reducir a la sustitución de los parámetros tipo y parámetros de valores de argumentos.
  • Reemplazamos declaración de retorno con una asignación a una nueva variable seguido de un salto al final del cuerpo de la función.
  • La llamada original a la función es reemplazada por este cuerpo.
  • Sin embargo, no hemos terminado. También debemos clonar todos los hijos de la función, reduciéndolos en beta también, y reparent los clones a la función de llamada.

La operación de clonación hace que sea extremadamente difícil para alinear las funciones recursivas. El truco habitual de mantener una lista de lo que ya está en progreso y simplemente verificar si ya estamos procesando esta llamada no funciona de forma ingenua porque la llamada recursiva ahora se mueve al código reducido en beta que se rellena en la llamada. función, y el objetivo de recursión puede haber cambiado a un hijo clonado. Sin embargo, ese niño, al llamar al padre, sigue llamando al padre original que llama a su hijo, y ahora el desenrollamiento de la recursión no se detendrá. Como mencioné, rompí esta regresión al permitir que se incluyera una llamada recursiva a un niño, lo que impidió que las repeticiones entre hermanos estuvieran en línea.

El costo de alineación se complica aún más por la necesidad de garbage collect funciones no utilizadas. Como la línea entrante es potencialmente exponencial, esto es esencial. Si todas las llamadas a una función están en línea, deberíamos deshacernos de la función si todavía no se ha introducido, de lo contrario perderemos tiempo en una función que ya no se usa. En realidad, hacer un seguimiento de quién llama es extremadamente difícil, porque al enderezar no estamos trabajando con una representación de función real, sino una función "desentrañada": por ejemplo, la lista de instrucciones se procesa secuencialmente y se crea una nueva lista, y en cualquier punto en el tiempo puede no haber una lista de instrucciones coherente.

En sus ML compilador Semanas Steven optó por utilizar una serie de pequeñas optimizaciones aplicadas en varias ocasiones, ya que esto hizo que las optimizaciones fácil de escribir y fácil de controlar, pero por desgracia esto no encuentra una gran cantidad de oportunidades de optimización en comparación con un algoritmo recursivo .

Problema n. ° 3: ¿Cuándo es seguro realizar una llamada a función?

Explicar este problema genéricamente: en un lenguaje funcional vago, los argumentos se envuelven en cierres y luego podemos alinear una aplicación; este es el modelo estándar para Haskell. Sin embargo, también explica por qué Haskell es tan lento. Los cierres no son necesarios si se conoce el argumento, entonces el parámetro se puede reemplazar directamente con su argumento donde se produce (este es el orden normal beta-reduction).

Sin embargo, si se sabe que la evaluación del argumento no es no terminante, se puede usar una evaluación entusiasta: el parámetro se le asigna el valor de la expresión una vez y luego se reutiliza. Un híbrido de estas dos técnicas es usar un cierre pero almacenar en caché el resultado dentro del objeto de cierre. Aún así, GHC no ha logrado producir código muy eficiente: es claramente muy difícil, especialmente si tiene una compilación por separado.

En Felix, tomé el enfoque opuesto. En lugar de exigir corrección y mejorar gradualmente la eficacia probando la semántica optimizada, confirmo que la optimización define la semántica. Esto garantiza el correcto funcionamiento del optimizador a expensas de la incertidumbre sobre cómo se comportará determinado código. La idea es proporcionar formas para que el programador obligue al optimizador a cumplir con la semántica deseada si la estrategia de optimización predeterminada es demasiado agresiva.

Por ejemplo, el modo de paso de parámetros predeterminado permite al compilador elegir si envolver el argumento en un cierre, reemplazar el parámetro con el argumento o asignar el argumento al parámetro. Si el programador quiere forzar un cierre, simplemente pueden pasar un cierre. Si el programador quiere forzar una evaluación entusiasta, marcan el parámetro var.

La complejidad aquí es mucho mayor que un lenguaje de programación funcional: Felix es un lenguaje de procedimientos con variables y punteros. También tiene clases de estilo Haskell. Esto hace que la rutina de entrada sea extremadamente compleja, por ejemplo, las instancias de clase de tipo reemplazan las funciones abstractas siempre que sea posible (debido a la especialización de tipo al llamar a una función polimórfica, puede ser posible encontrar una instancia mientras está en línea, entonces ahora tenemos una nueva función puede en línea).

Para que quede claro, tengo que agregar algunas notas más.

La alineación y otras optimizaciones tales como reducciones de términos definidas por el usuario, instancias de clases de tipos, comprobaciones de flujo de datos lineales para la eliminación de variables, optimización de compilación de cola, se realizan de una vez en una función determinada.

El problema de ordenar no es el orden para aplicar diferentes optimizaciones, el problema es ordenar las funciones.

Uso un algoritmo de muerte cerebral para detectar la recursión: construyo una lista de todo lo que usa directamente cada función, encuentro el cierre y luego verifico si la función está en el resultado. Tenga en cuenta que el conjunto de uso se acumula muchas veces durante la optimización, y esto es un serio cuello de botella.

Si una función es recursiva o no puede cambiar desafortunadamente. Una función recursiva puede volverse no recursiva después de la optimización de cola rec.Pero hay un caso mucho más difícil: crear una instancia "virtual" de una clase de tipo puede hacer que lo que parecía ser recursivo no recursivo.

En cuanto a las llamadas entre hermanos, el problema es que, dado f y g, donde f llama g y g llamadas f, realmente quiero alinear f en g, yg en f ... una vez. Mi regla de detención de regresión infinita es permitir solo la alineación de f en g si son recursivas recíprocas si f es un hijo de g, lo que excluye a los hermanos en línea.

Básicamente quiero "aplanar" todo el código "tanto como sea posible".

Respuesta

5

Me doy cuenta de que probablemente ya sepa todo esto, pero parece importante todavía escribirlo en su totalidad, al menos para una referencia posterior.

En la comunidad funcional, hay algo de literatura principalmente de la gente de GHC. Tenga en cuenta que consideran la incorporación como una transformación en el idioma de origen, mientras que parece que trabaja en un nivel inferior. Trabajar en el lenguaje fuente - o un lenguaje intermedio de semántica razonablemente similar - es, creo, una gran ayuda para la simplicidad y la corrección.

Para la cuestión del compilador de pedidos pasa, esto es bastante arcano. Aún en una configuración de Haskell, está la Tesis de Doctorado Compilation by Transformation in a Non-strict Functional Language que trata sobre el orden de diferentes pases de compilación (y también en línea).

También existe un artículo bastante reciente sobre Compilation by Equality Saturation que propone un enfoque novedoso para la optimización de los pedidos de pases. No estoy seguro de que haya demostrado aplicabilidad a gran escala, pero sin duda es una dirección interesante de explorar.

+0

Voy a consultar la Wiki de GHC. También vale la pena leer sobre MlTon. Sin embargo, pude haber sido un poco confuso: mi compilador no hace la mayoría de las optimizaciones en los pases: ya terminaron todos a la vez. – Yttrill

+0

gracias por el enlace Compilación por Igualdad de Saturación: ¡este es un enfoque muy agradable! – Yttrill

+0

Creo que es realmente importante alinear en el idioma de origen o equivalente, ya que hay mucho más margen para la optimización. Punto importante de @gasche anterior. –

1

En cuanto al caso de recursión, podría usar el algoritmo de Tarjan en su gráfico de llamadas para detectar clústeres de dependencia circulares y excluirlos de la alineación. No afectará las llamadas entre hermanos.

http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm

+0

El problema es: si f y g son hermanos y se llaman entre sí, ¿qué debo hacer? Quiero ** poner en línea f en g, y g en f. Una vez. – Yttrill

+0

Ok, ya veo. Pensé que no querías insertar f en g o g en f, sino en línea en f1 que llama f o g (lo cual tiene perfecto sentido). No estoy seguro si es una buena idea alinearlos una vez el uno con el otro. –

+0

No estoy seguro de nada, pero hay una razón perfectamente buena para alinear f en gy viceversa: permitir la optimización de la recuperación de la cola. Esto generalmente requiere una función con una llamada de autoasistencia para ser traducida en una asignación a los parámetros y un goto. – Yttrill