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?
exec
declaració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).locals()
persistir entre llamadaslocals
. Lo que está documentado y definitivamente no es un detalle de implementación es que ni siquieralocals
oglobals
puede 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,eval
yexec
definitivamente no son detalles de implementación, ya sea - vistazo a mi respuesta)Respuestas:
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
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:
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
foo
siempre es un número entero; entonces PyPy puede optimizar el código que busca el tipo defoo
en cada paso a través del ciclo, y a menudo incluso puede deshacerse del objeto Python que representa un número entero, yfoo
puede 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.
fuente
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 #.
fuente