Todos sabemos que Python es mucho más lento que los lenguajes de programación de tipo estático como C, C++, Java y algunos lenguajes dinámicos como JavaScript y PHP. Veamos las razones por las que Python es mucho más lento en comparación con estos lenguajes y qué podemos hacer para aumentar su velocidad de ejecución.
La implementación predeterminada de Python ' CPython ' usa GIL (Global Interpreter Lock) para ejecutar exactamente un subproceso al mismo tiempo, incluso si se ejecuta en un procesador de varios núcleos, ya que GIL funciona solo en un núcleo, independientemente de la cantidad de núcleos presentes en el máquina. Cada núcleo de la CPU tiene su propia GIL, por lo que una CPU de cuatro núcleos tendrá 4 GIL ejecutándose por separado con su propio intérprete. Para hacer que nuestros programas de python se ejecuten en paralelo, usamos subprocesos múltiples y procesamiento múltiple.
Los subprocesos múltiples no hacen una gran diferencia en el tiempo de ejecución, ya que utilizan el mismo espacio de memoria y un solo GIL, por lo que las tareas vinculadas a la CPU no tienen un impacto en el rendimiento de los programas de subprocesos múltiples, ya que el bloqueo se comparte entre los subprocesos. en el mismo núcleo y solo se ejecuta un subproceso mientras esperan que otras tareas terminen de procesarse. Además, los subprocesos usan la misma memoria, por lo que se deben tomar precauciones o dos subprocesos escribirán en la misma memoria al mismo tiempo. Esta es la razón por la cual se requiere el bloqueo de intérprete global.
El multiprocesamiento aumenta el rendimiento del programa ya que cada proceso de Python obtiene su propio intérprete de Python y espacio de memoria, por lo que el GIL no será un problema. Pero también aumenta los gastos generales de gestión de procesos, ya que varios procesos son más pesados que varios subprocesos. Además, necesitamos compartir objetos de una memoria a otra cada vez que actualizamos objetos en una memoria, ya que la memoria no está vinculada entre sí y realiza tareas por separado.
Dado que GIL permite que solo se ejecute un subproceso a la vez, incluso en una arquitectura de subprocesos múltiples con más de un núcleo de CPU, GIL se ha ganado la reputación de ser una característica "infame" de Python. Por lo tanto, esto limita la velocidad de ejecución de los programas de Python y no utiliza los recursos proporcionados al máximo.
Entonces, ¿por qué no eliminamos GIL? CPython utiliza el recuento de referencias para la gestión de la memoria. Significa que los objetos creados en CPython tienen una variable de recuento de referencias que realiza un seguimiento del número de referencias que apuntan al objeto. Cuando este conteo llega a cero, se libera la memoria ocupada por el objeto.
Si eliminamos GIL de CPython, la variable de recuento de referencia ya no estará protegida, ya que dos subprocesos pueden aumentar o disminuir su valor simultáneamente. Y si esto sucede, puede causar una fuga de memoria que nunca se libera o, lo que es peor, una liberación incorrecta de la memoria mientras aún existe una referencia a ese objeto. Esto puede causar bloqueos u otros errores "extraños" en nuestros programas de Python.
Además, ha habido algunos intentos de eliminar el GIL de CPython, pero la sobrecarga adicional para máquinas de un solo subproceso generalmente era demasiado grande. En realidad, algunos casos pueden ser más lentos incluso en máquinas multiprocesador debido a la contención de bloqueo.
Existen enfoques alternativos a GIL, como Jython y IronPython , que utilizan el enfoque de subprocesos de su VM subyacente, en lugar de un enfoque GIL.
Para concluir, GIL no es un gran problema para nosotros en este momento, ya que los programas de Python con un GIL se pueden diseñar para usar procesos separados para lograr un paralelismo total, ya que cada proceso tiene su propio intérprete y, a su vez, tiene su propio GIL.
Beneficios de tener GIL en la implementación de Python :
Todos conocemos Python como un lenguaje de programación de tipo dinámico en el que no necesitamos especificar tipos de datos variables al asignar variables. El tipo de datos se asigna a la variable en tiempo de ejecución, por lo que cada vez que se lee, se escribe o se hace referencia a la variable, se comprueba su tipo de datos y la memoria se asigna en consecuencia.
Mientras que los lenguajes de programación de tipo estático tienen una ventaja sobre esto, ya que los tipos de datos ya se conocen, por lo que no necesitan verificar el tipo de datos cada vez que se usa la variable en el programa. Esto les ahorra mucho tiempo y hace que toda la ejecución sea más rápida.
El diseño del lenguaje Python nos permite hacer que casi cualquier cosa sea dinámica. Podemos reemplazar los métodos en los objetos en tiempo de ejecución, podemos parchear las llamadas al sistema de bajo nivel a un valor declarado en tiempo de ejecución. Casi todo es posible. Entonces, no tener que declarar el tipo no es lo que hace que Python sea lento, es este diseño lo que hace que sea increíblemente difícil optimizar Python.
Tan pronto como ejecutamos nuestro programa Python, el archivo .py del código fuente se compila primero usando CPython (escrito en el lenguaje de programación 'C') en el archivo .pyc
de código de bytes intermedio guardado en la carpeta __pycache__
(Python 3) y luego interpretado por Python Virtual Machine al código de máquina.
Dado que CPython usa un intérprete que ejecuta el código de bytes generado directamente en tiempo de ejecución, esto hace que la ejecución sea mucho más lenta ya que cada línea se interpreta durante la ejecución del programa. Mientras que otros lenguajes de programación como C, C++ se compilan directamente en código de máquina antes de que se lleve a cabo la ejecución mediante la compilación Ahead of time (AOT) . Además, Java se compila en un "lenguaje intermedio" y la máquina virtual de Java lee el código de bytes y lo compila justo a tiempo (JIT) en código de máquina. El lenguaje intermedio común (CIL) de .NET es el mismo, el tiempo de ejecución de lenguaje común (CLR) de .NET , utiliza la compilación justo a tiempo (JIT) para el código de la máquina.
Entendemos que la compilación AOT es más rápida que la interpretación, ya que el programa ya se ha compilado en el código legible por máquina antes de que tenga lugar la ejecución. Pero , ¿cómo logra la compilación JIT ejecutar programas más rápido que los programas implementados por CPython?
La compilación JIT es una combinación de los dos enfoques tradicionales para la traducción a código de máquina (compilación anticipada (AOT) e interpretación ) y combina algunas ventajas y desventajas de ambos. Entonces, la compilación JIT optimiza nuestro programa al compilar ciertas partes del programa que se usan con frecuencia y se ejecuta con el resto del código en el tiempo de ejecución del programa.
Algunas implementaciones de Python como PyPy utilizan la compilación JIT, que es más de 4 veces más rápida que CPython. Entonces, ¿por qué CPython no usa JIT?
También hay desventajas de JIT, una de ellas es un retraso en el tiempo de inicio . Las implementaciones que usan JIT tienen un tiempo de arranque significativamente más lento en comparación con CPython. CPython es una implementación de propósito general para desarrollar programas y proyectos de línea de comandos (CLI) que no requieren mucho trabajo pesado de la CPU. Existía la posibilidad de usar JIT en CPython, pero se ha estancado en gran medida debido a su implementación difícil y la falta de flexibilidad en Python.
"Si desea que su código se ejecute más rápido, probablemente debería usar PyPy". — Guido van Rossum (creador de Python)
Se afirma que PyPy es la implementación más rápida para Python con el soporte de bibliotecas populares de Python como Django y es altamente compatible con el código de Python existente. PyPy tiene un GIL y usa la compilación JIT, por lo que combina las ventajas de ambos, lo que hace que la ejecución general sea mucho más rápida que CPython**.** Varios estudios han sugerido que es aproximadamente 7,5 veces más rápido que CPython.
PyPy primero toma nuestro código fuente de Python y lo convierte a RPython , que es un subconjunto restringido de tipo estático de Python. RPython es más fácil de compilar en un código más eficiente ya que es un lenguaje de tipo estático. PyPy luego traduce el código RPython generado en una forma de código de bytes, junto con un intérprete escrito en el lenguaje de programación 'C'. Gran parte de este código se compila luego en código de máquina y el código de bytes se ejecuta en el intérprete compilado.
Aquí hay una representación visual de esta implementación:
También permite recolectores de basura conectables , así como opcionalmente habilita funciones de Stackless Python . Finalmente, incluye un generador justo a tiempo (JIT) que crea un compilador justo a tiempo en el intérprete, con algunas anotaciones en el código fuente del intérprete. El compilador JIT generado es un JIT de seguimiento .
Esta fue una breve explicación de cómo funciona la implementación, si tiene curiosidad por saber más sobre PyPy, puede leer más aquí .
Como discutimos que la desventaja de JIT es su retraso en el tiempo de inicio, PyPy sigue la suite. Además, PyPy es incompatible con muchas extensiones C porque CPython está escrito en el lenguaje de programación 'C' y las extensiones de terceros en PyPI se aprovechan de esto. Numpy sería un buen ejemplo, gran parte de Numpy está escrito en código C optimizado. Cuando pip install numpy
, usa nuestro compilador C local y crea una biblioteca binaria para que la use nuestro tiempo de ejecución de Python.
PyPy está escrito en Python , por lo que debemos asegurarnos de que los módulos necesarios para nuestro proyecto sean compatibles con PyPy antes de implementarlo en nuestro proyecto.
Estas fueron las razones para no usar PyPy como implementación predeterminada en Python. Además de PyPy, hay muchas otras implementaciones disponibles para Python que se pueden usar alternativamente para hacer que Python se ejecute más rápido para que pueda elegir la que más le convenga.
Los hallazgos que he presentado sugieren que Python es de hecho un lenguaje lento debido a su naturaleza dinámica en comparación con otros lenguajes de tipo estático como C, C++, Java. Pero, ¿debemos preocuparnos mucho por eso?
Probablemente no, ya que todos sabemos cuánto tiempo de desarrollo se ahorra al usar Python en nuestros proyectos. Las empresas emergentes ya están utilizando Python de manera extensiva para sus proyectos solo para que su producto esté en el mercado lo antes posible. Esto les ahorra una gran cantidad de costos de mano de obra y horas de trabajo invertidas en un solo producto. Los marcos como Django han hecho posible el desarrollo de pila completa con muchas características esenciales ya proporcionadas.
Los desarrolladores de Python ahora emplean una implementación óptima para Python si el rendimiento es una limitación para ellos mientras trabajan en Machine Learning, Big Data, Inteligencia artificial en general. Las posibilidades son infinitas cuando se trata de usar un lenguaje moderno y dinámico con un amplio soporte de más de 100 000 bibliotecas disponibles en Python Package Index (PyPI) en la actualidad. Esto hace que los desarrolladores trabajen más fácil y rápido al mismo tiempo.
Si desea obtener más información sobre Python GIL, las implementaciones de Python, el código de bytes de Python y cómo funcionan, le recomiendo estos recursos: