¿Cuáles son los desafíos relacionados con escribir al compilar un compilador para un lenguaje escrito dinámicamente?

9

En esta charla , Guido van Rossum habla (27:30) sobre los intentos de escribir un compilador para el código Python, comentando al respecto diciendo:

Resulta que no es tan fácil escribir un compilador que mantenga todas las buenas propiedades de escritura dinámica y también mantenga la corrección semántica de su programa, de modo que realmente haga lo mismo sin importar qué tipo de rareza haga en algún lugar debajo de las cubiertas y realmente se ejecute más rápido

¿Cuáles son los (posibles) desafíos relacionados con escribir al compilar un compilador para un lenguaje escrito dinámicamente como Python?

sintagma
fuente
En este caso, la escritura dinámica no es casi el mayor problema. Para python es un alcance dinámico.
SK-logic
Vale la pena señalar que otras personas han argumentado que construir una escritura dinámica en la plataforma es la respuesta correcta aquí. Microsoft invirtió mucho dinero en DLR exactamente por esta razón, y NeXT / Apple ha estado a mitad de camino en ese tren durante décadas. Eso no ayuda a CPython, pero IronPython demuestra que puede compilar estáticamente Python de manera efectiva, y PyPy demuestra que no tiene que hacerlo.
abarnert
2
@ SK-logic Ámbito dinámico en Python? La última vez que lo verifiqué, todas las construcciones en el lenguaje usan el alcance léxico.
1
@ SK-logic Puede crear código dinámicamente y ejecutarlo, pero ese código también se ejecuta con un ámbito léxico. Para cada variable en un programa Python, puede determinar fácilmente a qué alcance pertenece una variable simplemente inspeccionando el AST. Puede estar pensando en la execdeclaración , que desapareció desde 3.0 y, por lo tanto, está fuera de mi consideración (y probablemente de Guido, ya que la charla es de 2012). ¿Podrías dar un ejemplo? Y su definición de "alcance dinámico", si es [diferente del mío] (en.wikipedia.org/wiki/Dynamic_scoping).
1
@ SK-logic Lo único que es un detalle de implementación para mí son los cambios para devolver el valor de locals()persistir entre llamadas locals. Lo que está documentado y definitivamente no es un detalle de implementación es que ni siquiera localso globalspuede cambiar en el que alcance cada variable se busca en. Para cada uso de una variable, el ámbito al que se refiere es, se determina estáticamente. Lo que lo hace decididamente de ámbito léxico. (Y por cierto, evaly execdefinitivamente no son detalles de implementación, ya sea - vistazo a mi respuesta)

Respuestas:

16

Simplificaste demasiado la declaración de Guido al formular tu pregunta. El problema no es escribir un compilador para un lenguaje de tipo dinámico. El problema es escribir uno que sea (criterio 1) siempre correcto, (criterio 2) mantiene la escritura dinámica y (criterio 3) es notablemente más rápido para una cantidad significativa de código.

Es fácil implementar el 90% (error de criterio 1) de Python y ser consistentemente rápido en ello. Del mismo modo, es fácil crear una variante de Python más rápida con tipeo estático (criterios de error 2). Implementar 100% también es fácil (en la medida en que implementar un lenguaje tan complejo es fácil), pero hasta ahora todas las formas fáciles de implementar resultan relativamente lentas (criterios de falla 3).

Implementar un intérprete más JIT que sea correcto, implemente todo el lenguaje y sea más rápido para algunos códigos resulta factible, aunque significativamente más difícil (cf. PyPy) y solo si automatizas la creación del compilador JIT (Psyco lo hizo sin él) , pero estaba muy limitado en qué código podría acelerar). Pero tenga en cuenta que esto está explícitamente fuera de alcance, ya que estamos hablando de estática(también conocido como compiladores de antemano). Solo menciono esto para explicar por qué su enfoque no funciona para compiladores estáticos (o al menos no hay un contraejemplo existente): primero tiene que interpretar y observar el programa, luego generar código para una iteración específica de un bucle (u otro código lineal ruta), luego optimice el infierno basado en suposiciones solo verdaderas para esa iteración específica (o al menos, no para todas las iteraciones posibles). La expectativa es que muchas ejecuciones posteriores de ese código también coincidan con la expectativa y, por lo tanto, se beneficien de las optimizaciones. Se agregan algunos controles (relativamente baratos) para asegurar la corrección. Para hacer todo esto, necesita una idea de en qué especializarse, y una implementación lenta pero general a la que recurrir. Los compiladores de AOT no tienen ninguno. No pueden especializarse en absolutobasado en el código que no pueden ver (por ejemplo, código cargado dinámicamente), y especializarse descuidadamente significa generar más código, lo que tiene una serie de problemas (utilización de icache, tamaño binario, tiempo de compilación, ramas adicionales).

Implementar un compilador AOT que implemente correctamente todo el lenguaje también es relativamente fácil: generar código que llame al tiempo de ejecución para hacer lo que haría el intérprete cuando se alimenta con este código. Nuitka (en su mayoría) hace esto. Sin embargo, esto no produce muchos beneficios de rendimiento (criterios de error 3), ya que aún tiene que hacer tanto trabajo innecesario como un intérprete, salvo por enviar el código de bytes al bloque de código C que hace lo que compiló. Pero eso es solo un costo bastante pequeño, lo suficientemente significativo como para que valga la pena optimizarlo en un intérprete existente, pero no lo suficientemente significativo como para justificar una implementación completamente nueva con sus propios problemas.

¿Qué se necesitaría para cumplir con los tres criterios? No tenemos idea Existen algunos esquemas de análisis estático que pueden extraer información sobre tipos concretos, flujo de control, etc. de los programas de Python. Los que producen datos precisos más allá del alcance de un solo bloque básico son extremadamente lentos y necesitan ver todo el programa, o al menos la mayor parte. Aún así, no puede hacer mucho con esa información, aparte de quizás optimizar algunas operaciones en tipos incorporados.

¿Porque eso? Para decirlo sin rodeos, un compilador elimina la capacidad de ejecutar el código Python cargado en tiempo de ejecución (criterio de error 1), o no hace ninguna suposición que pueda ser invalidada por ningún código Python. Desafortunadamente, eso incluye casi todo lo útil para optimizar programas: los globales que incluyen funciones pueden ser rebotados, las clases pueden ser mutadas o reemplazadas por completo, los módulos también pueden modificarse arbitrariamente, la importación puede ser secuestrada de varias maneras, etc. Una sola cadena pasada a eval, exec, __import__o muchas otras funciones, pueden hacer algo de eso. En efecto, eso significa que casi no se pueden aplicar grandes optimizaciones, produciendo pocos beneficios de rendimiento (criterios de error 3). De vuelta al párrafo anterior.


fuente
4

El problema más difícil es averiguar qué tipo tiene todo en un momento dado.

En un lenguaje estático como C o Java, una vez que haya visto la declaración de tipo, sabrá qué es ese objeto y qué puede hacer. Si se declara una variable int, es un número entero. No es, por ejemplo, una referencia de función invocable.

En Python, puede ser. Esto es horrible Python, pero legal:

i = 2
x = 3 + i

def prn(s):
    print(s)

i = prn
i(x)

Ahora, este ejemplo es bastante estúpido, pero ilustra la idea general.

De manera más realista, puede reemplazar una función incorporada con una función definida por el usuario que hace algo ligeramente diferente (como una versión que registra sus argumentos cuando la llama).

PyPy usa la compilación Just-In-Time después de ver lo que hace el código, y esto le permite a PyPy acelerar mucho las cosas. PyPy puede ver un ciclo y verificar que cada vez que se ejecuta el ciclo, la variable foosiempre es un número entero; entonces PyPy puede optimizar el código que busca el tipo de fooen cada paso a través del ciclo, y a menudo incluso puede deshacerse del objeto Python que representa un número entero, y foopuede convertirse en un número que se encuentra en un registro en la CPU. Así es como PyPy puede ser más rápido que CPython; CPython realiza la búsqueda de tipos lo más rápido posible, pero ni siquiera la búsqueda es aún más rápida.

No conozco los detalles, pero recuerdo que había un proyecto llamado Unladen Swallow que intentaba aplicar la tecnología de compilación estática para acelerar Python (usando LLVM). Es posible que desee buscar en Google Unladen Swallow y ver si puede encontrar una discusión sobre por qué no funcionó como esperaban.

steveha
fuente
Unladen Swallow no se trataba de compilación estática o tipos estáticos; la eventual marcha fue efectivamente portar el intérprete de CPython, con toda su dinámica, a LLVM, con un nuevo JIT elegante (algo así como Parrot, o DLR para .NET ... o PyPy, realmente), aunque lo que realmente terminaron hacer fue encontrar muchas optimizaciones locales dentro de CPython (algunas de las cuales llegaron a la línea principal 3.x). Shedskin es probablemente el proyecto en el que está pensando que usó la inferencia de tipo estático para compilar estáticamente Python (aunque para C ++, no directamente para el código nativo).
abarnert
Uno de los autores de Unladen Swallow, Reid Kleckner, publicó una Retrospectiva de Unladen Swallow , que puede valer la pena leer en este contexto, aunque en realidad se trata más de desafíos de gestión y patrocinio que técnicos.
abarnert
0

Como dice la otra respuesta, el problema clave es averiguar la información de tipo. En la medida en que pueda hacerlo estáticamente, puede generar directamente un buen código.

Pero incluso si no puede hacerlo estáticamente, aún puede generar código razonable, solo en tiempo de ejecución, cuando obtiene información de tipo real . Esta información a menudo resulta ser estable, o tener como máximo algunos valores diferentes para cualquier entidad en particular en un punto de código particular. El lenguaje de programación SELF fue pionero en muchas de las ideas de la colección agresiva de tipos de tiempo de ejecución y la generación de código de tiempo de ejecución. Sus ideas son ampliamente utilizadas en compiladores modernos basados ​​en JIT como Java y C #.

Ira Baxter
fuente