Cinder es la versión de producción interna de CPython 3.10 orientada al rendimiento de Meta. Contiene una serie de optimizaciones de rendimiento, incluido el almacenamiento en caché en línea de código de bytes, evaluación entusiasta de corrutinas, un JIT de método a la vez y un compilador de código de bytes experimental que utiliza anotaciones de tipo para emitir código de bytes especializado en tipos que funciona mejor en el JIT.
Cinder está impulsando Instagram, donde comenzó, y se usa cada vez más en más y más aplicaciones Python en Meta.
Para obtener más información sobre CPython, consulte README.cpython.rst
.
Respuesta corta: no.
Hemos puesto Cinder a disposición del público para facilitar la conversación sobre la posibilidad de transferir parte de este trabajo a CPython y reducir la duplicación de esfuerzos entre las personas que trabajan en el rendimiento de CPython.
Cinder no se pule ni se documenta para el uso de nadie más. No tenemos el deseo de que se convierta en una alternativa a CPython. Nuestro objetivo al hacer que este código esté disponible es un CPython unificado y más rápido. Entonces, si bien ejecutamos Cinder en producción, si eliges hacerlo, estarás solo. No podemos comprometernos a corregir informes de errores externos ni a revisar solicitudes de extracción. Nos aseguramos de que Cinder sea lo suficientemente estable y rápido para nuestras cargas de trabajo de producción, pero no garantizamos su estabilidad, corrección o rendimiento para cargas de trabajo o casos de uso externos.
Dicho esto, si tiene experiencia en tiempos de ejecución de lenguajes dinámicos y tiene ideas para hacer que Cinder sea más rápido; o si trabaja en CPython y desea utilizar Cinder como inspiración para mejoras en CPython (o ayudar a actualizar partes de Cinder a CPython), comuníquese con nosotros; ¡Nos encantaría charlar!
Cinder debería construirse como CPython; configure
y make -j
. Sin embargo, como la mayor parte del desarrollo y uso de Cinder ocurre en el contexto altamente específico de Meta, no lo utilizamos mucho en otros entornos. Como tal, la forma más confiable de construir y ejecutar Cinder es reutilizar la configuración basada en Docker de nuestro flujo de trabajo de GitHub CI.
Si solo desea obtener un Cinder que funcione sin compilarlo usted mismo, nuestra imagen de Runtime Docker será la más fácil (¡no se necesita clonar el repositorio!):
docker run -it --rm ghcr.io/facebookincubator/cinder-runtime:cinder-3.10
Si quieres construirlo tú mismo:
git clone https://github.com/facebookincubator/cinder
docker run -v "$PWD/cinder:/vol" -w /vol -it --rm ghcr.io/facebookincubator/cinder/python-build-env:latest bash
./configure && make
Tenga en cuenta que Cinder solo se compila o prueba en Linux x64; cualquier otra cosa (incluido macOS) probablemente no funcione. La imagen de Docker de arriba está basada en Fedora Linux y está construida a partir de un archivo de especificaciones de Docker en el repositorio de Cinder: .github/workflows/python-build-env/Dockerfile
.
Hay algunos objetivos de prueba nuevos que podrían resultar interesantes. make testcinder
es prácticamente lo mismo que make test
excepto que omite algunas pruebas que son problemáticas en nuestro entorno de desarrollo. make testcinder_jit
ejecuta el conjunto de pruebas con JIT completamente habilitado, por lo que todas las funciones están ejecutadas en JIT. make testruntime
ejecuta un conjunto de pruebas unitarias gtest de C++ para JIT. Y make test_strict_module
ejecute un conjunto de pruebas para módulos estrictos (ver más abajo).
Tenga en cuenta que estos pasos producen un binario de Cinder Python sin las optimizaciones PGO/LTO habilitadas, así que no espere utilizar estas instrucciones para acelerar cualquier carga de trabajo de Python.
Cinder Explorer es un patio de juegos en vivo, donde puedes ver cómo Cinder compila el código Python desde el código fuente hasta el ensamblador. ¡Te invitamos a probarlo! No dude en presentar solicitudes de funciones e informes de errores. Tenga en cuenta que Cinder Explorer, como el resto de esto, "soporta" en la medida de lo posible.
Instagram utiliza una arquitectura de servidor web multiproceso; el proceso principal se inicia, realiza el trabajo de inicialización (por ejemplo, cargar código) y bifurca decenas de procesos de trabajo para manejar las solicitudes de los clientes. Los procesos de trabajo se reinician periódicamente por diversos motivos (por ejemplo, pérdidas de memoria, implementaciones de código) y tienen una vida útil relativamente corta. En este modelo, el sistema operativo debe copiar la página completa que contiene un objeto que se asignó en el proceso principal cuando se modifica el recuento de referencias del objeto. En la práctica, los objetos asignados en el proceso principal sobreviven a los trabajadores; todo el trabajo relacionado con el conteo de referencias es innecesario.
Instagram tiene una base de código Python muy grande y la sobrecarga debido a la copia en escritura del recuento de referencias de objetos de larga duración resultó ser significativa. Desarrollamos una solución llamada "instancias inmortales" para proporcionar una forma de excluir objetos del recuento de referencias. Consulte Incluir/object.h para obtener más detalles. Esta característica se controla definiendo Py_IMMORTAL_INSTANCES y está habilitada de forma predeterminada en Cinder. Esta fue una gran victoria para nosotros en producción (~5%), pero hace que el código en línea recta sea más lento. Las operaciones de recuento de referencias se producen con frecuencia y deben comprobar si un objeto participa o no en el recuento de referencias cuando esta función está habilitada.
"Shadowcode" o "shadow bytecode" es nuestra implementación de un intérprete especializado. Observa casos optimizables particulares en la ejecución de códigos de operación genéricos de Python y (para funciones activas) reemplaza dinámicamente esos códigos de operación con versiones especializadas. El núcleo del código de sombra se encuentra en Shadowcode/shadowcode.c
, aunque las implementaciones para los códigos de bytes especializados están en Python/ceval.c
con el resto del bucle de evaluación. Las pruebas específicas de Shadowcode se encuentran en Lib/test/test_shadowcode.py
.
Es similar en espíritu al intérprete adaptativo especializado (PEP-659) que se integrará en CPython 3.11.
El servidor de Instagram es una carga de trabajo pesada asíncrona, donde cada solicitud web puede desencadenar cientos de miles de tareas asíncronas, muchas de las cuales pueden completarse sin suspensión (por ejemplo, gracias a los valores memorizados).
Ampliamos el protocolo vectorcall para pasar un nuevo indicador, Ci_Py_AWAITED_CALL_MARKER
, que indica que la persona que llama está esperando inmediatamente esta llamada.
Cuando se usa con llamadas a funciones asíncronas que se esperan inmediatamente, podemos evaluar inmediatamente (con entusiasmo) la función llamada, hasta su finalización o hasta su primera suspensión. Si la función se completa sin suspenderse, podemos devolver el valor inmediatamente, sin asignaciones de montón adicionales.
Cuando se usa con recopilación asíncrona, podemos evaluar inmediatamente (con entusiasmo) el conjunto de esperas pasadas, evitando potencialmente el costo de creación y programación de múltiples tareas para corrutinas que podrían completarse sincrónicamente, futuros completados, valores memorizados, etc.
Estas optimizaciones dieron como resultado una mejora significativa (~5%) en la eficiencia de la CPU.
Esto se implementa principalmente en Python/ceval.c
, a través de un nuevo indicador de llamada vectorial Ci_Py_AWAITED_CALL_MARKER
, que indica que la persona que llama está esperando inmediatamente esta llamada. Busque usos de la macro IS_AWAITED()
y este indicador de llamada vectorial.
Cinder JIT es un JIT personalizado de método a la vez implementado en C++. Se habilita mediante el indicador -X jit
o la variable de entorno PYTHONJIT=1
. Es compatible con casi todos los códigos de operación de Python y puede lograr mejoras de velocidad de 1,5 a 4 veces en muchos puntos de referencia de rendimiento de Python.
De forma predeterminada, cuando está habilitado, compilará JIT cada función que se llame, lo que bien puede hacer que su programa sea más lento, no más rápido, debido a la sobrecarga de la compilación JIT de funciones raramente llamadas. La opción -X jit-list-file=/path/to/jitlist.txt
o PYTHONJITLISTFILE=/path/to/jitlist.txt
puede apuntar a un archivo de texto que contiene nombres de funciones completos (en el formato path.to.module:funcname
o path.to.module:ClassName.method_name
), uno por línea, que debe estar compilado en JIT. Usamos esta opción para compilar solo un conjunto de funciones activas derivadas de datos de perfiles de producción. (Un enfoque más típico para un JIT sería compilar dinámicamente funciones a medida que se observa que se llaman con frecuencia. Todavía no ha valido la pena para nosotros implementar esto, ya que nuestra arquitectura de producción es un servidor web previo a la bifurcación, y para razones por las que queremos compartir memoria, deseamos realizar toda nuestra compilación JIT por adelantado en el proceso inicial antes de que los trabajadores sean bifurcados, lo que significa que no podemos observar la carga de trabajo en proceso antes de decidir qué funciones compilar JIT).
El JIT se encuentra en el directorio Jit/
y sus pruebas de C++ se encuentran en RuntimeTests/
(ejecútelas con make testruntime
). También hay algunas pruebas de Python en Lib/test/test_cinderjit.py
; estos no pretenden ser exhaustivos, ya que ejecutamos todo el conjunto de pruebas de CPython bajo JIT a través de make testcinder_jit
; cubren casos extremos JIT que de otro modo no se encuentran en el conjunto de pruebas de CPython.
Consulte Jit/pyjit.cpp
para conocer otras opciones -X
y variables de entorno que influyen en el comportamiento de JIT. También hay un módulo cinderjit
definido en ese archivo que expone algunas utilidades JIT al código Python (por ejemplo, forzar la compilación de una función específica, verificar si una función está compilada, deshabilitar el JIT). Tenga en cuenta que cinderjit.disable()
sólo desactiva la compilación futura; compila inmediatamente todas las funciones conocidas y mantiene las funciones compiladas JIT existentes.
El JIT primero reduce el código de bytes de Python a una representación intermedia de alto nivel (HIR); esto se implementa en Jit/hir/
. HIR se asigna razonablemente de cerca al código de bytes de Python, aunque es una máquina de registro en lugar de una máquina de pila, tiene un nivel un poco más bajo, está escrito y algunos detalles que están ocultos por el código de bytes de Python pero que son importantes para el rendimiento (en particular, el recuento de referencias) son expuesto explícitamente en HIR. HIR se transforma al formato SSA, se realizan algunas pasadas de optimización y luego se insertan automáticamente operaciones de recuento de referencias de acuerdo con los metadatos sobre el recuento y los efectos de memoria de los códigos de operación HIR.
Luego, HIR se reduce a una representación intermedia de bajo nivel (LIR), que es una abstracción sobre el ensamblaje, implementada en Jit/lir/
. En LIR registramos la asignación, algunos pases de optimización adicionales y finalmente LIR se baja al ensamblaje (en Jit/codegen/
) usando la excelente biblioteca asmjit.
El JIT se encuentra en sus primeras etapas. Si bien ya puede eliminar la sobrecarga del bucle del intérprete y ofrece importantes mejoras de rendimiento para muchas funciones, sólo hemos comenzado a arañar la superficie de posibles optimizaciones. Muchas optimizaciones comunes del compilador aún no se han implementado. Nuestra priorización de optimizaciones está determinada en gran medida por las características de la carga de trabajo de producción de Instagram.
Los módulos estrictos son algunas cosas juntas en una:
1. Un analizador estático capaz de validar que la ejecución del código de nivel superior de un módulo no tendrá efectos secundarios visibles fuera de ese módulo.
2. Un tipo StrictModule
inmutable que se puede utilizar en lugar del tipo de módulo predeterminado de Python.
3. Un cargador de módulos de Python capaz de reconocer módulos seleccionados en modo estricto (a través de una import __strict__
en la parte superior del módulo), analizarlos para validar que no haya efectos secundarios de importación y completarlos en sys.modules
como un objeto StrictModule
.
Static Python es un compilador de código de bytes que utiliza anotaciones de tipo para emitir código de bytes de Python especializado y verificado. Utilizado junto con Cinder JIT, puede ofrecer un rendimiento similar a MyPyC o Cython en muchos casos, al tiempo que ofrece una experiencia de desarrollador puramente Python (sintaxis normal de Python, sin paso de compilación adicional). Static Python más Cinder JIT logra 18 veces el rendimiento del CPython estándar en una versión mecanografiada del benchmark Richards. En Instagram hemos utilizado con éxito Static Python en producción para reemplazar todos los módulos Cython en nuestra base de código de servidor web principal, sin regresión en el rendimiento.
El compilador Static Python está construido sobre el módulo compiler
de Python que se eliminó de la biblioteca estándar en Python 3 y desde entonces se ha mantenido y actualizado externamente; este compilador está incorporado a Cinder en Lib/compiler
. El compilador Static Python se implementa en Lib/compiler/static/
y sus pruebas están en Lib/test/test_compiler/test_static.py
.
A las clases definidas en los módulos Static Python se les asignan automáticamente espacios escritos (según la inspección de sus atributos de clase escritos y asignaciones anotadas en __init__
), y las cargas y almacenes de atributos en instancias de estos tipos utilizan nuevos códigos de operación STORE_FIELD
y LOAD_FIELD
, que en el JIT se vuelven directos. carga/almacena desde/hacia un desplazamiento de memoria fijo en el objeto, sin la dirección indirecta de LOAD_ATTR
o STORE_ATTR
. Las clases también obtienen tablas virtuales de sus métodos, para que las utilicen los códigos de operación INVOKE_*
que se mencionan a continuación. El soporte de tiempo de ejecución para estas funciones se encuentra en StaticPython/classloader.h
y StaticPython/classloader.c
.
Una función estática de Python comienza con un prólogo oculto que verifica que los tipos de argumentos proporcionados coincidan con las anotaciones de tipo y genera TypeError
si no. Las llamadas desde una función estática de Python a otra función estática de Python omitirán este código de operación (ya que los tipos ya están validados por el compilador). Las llamadas estáticas a estáticas también pueden evitar gran parte de la sobrecarga de una llamada de función típica de Python. Emitimos un código de operación INVOKE_FUNCTION
o INVOKE_METHOD
que lleva consigo metadatos sobre la función o método llamado; esto más módulos opcionalmente inmutables (a través de StrictModule
) y tipos (a través de cinder.freeze_type()
, que actualmente aplicamos a todos los tipos en módulos estrictos y estáticos en nuestro cargador de importación, pero que en el futuro pueden convertirse en una parte inherente de Static Python) y compilar -El conocimiento en tiempo de la firma de la persona que llama nos permite (en JIT) convertir muchas llamadas a funciones de Python en llamadas directas a una dirección de memoria fija utilizando la convención de llamadas x64, con un poco más de sobrecarga que una llamada a funciones de C.
Static Python todavía se escribe gradualmente y admite código que solo está parcialmente anotado o utiliza tipos desconocidos al recurrir al comportamiento dinámico normal de Python. En algunos casos (por ejemplo, cuando se devuelve un valor de tipo estáticamente desconocido desde una función con una anotación de retorno), se inserta un código de operación CAST
en tiempo de ejecución que generará TypeError
si el tipo en tiempo de ejecución no coincide con el tipo esperado.
Static Python también admite nuevos tipos de enteros de máquina, bools, dobles y vectores/matrices. En JIT, estos se manejan como valores sin caja y, por ejemplo, la aritmética entera primitiva evita toda la sobrecarga de Python. Algunas operaciones en tipos integrados (por ejemplo, lista o subíndice de diccionario o len()
) también están optimizadas.
Cinder admite la adopción gradual de módulos estáticos a través de un cargador de módulos estricto/estático que puede detectar automáticamente módulos estáticos y cargarlos como estáticos con compilación entre módulos. El cargador buscará anotaciones import __static__
e import __strict__
en la parte superior de un archivo y compilará los módulos adecuadamente. Para habilitar el cargador, tiene una de tres opciones:
1. Instale explícitamente el cargador en el nivel superior de su aplicación from cinderx.compiler.strict.loader import install; install()
.
PYTHONINSTALLSTRICTLOADER=1
en su entorno../python -X install-strict-loader application.py
. Alternativamente, puede compilar todo el código estáticamente usando ./python -m compiler --static some_module.py
, que compilará el módulo como Python estático y lo ejecutará.
Consulte CinderDoc/static_python.rst
para obtener documentación más detallada.