miércoles, 18 de julio de 2018

¿Por qué Python es tan lento?



Python está en auge en popularidad. Se usa en DevOps, Data Science, Desarrollo web y Seguridad.

Sin embargo, no gana ninguna medalla por velocidad.

En comparación con otros lenguajes como Java, C #, Go, JavaScript, C ++, Python es uno de los más lentos. Esto incluye compiladores JIT (C #, Java) y AOT (C, C ++), así como lenguajes interpretados como JavaScript.

Ojo! Cuando decimos "Python", estamos hablando de la implementación de referencia del lenguaje.

Podemos nombrar las siguientes teorías de porque python no es tan rapido:


  • Es el GIL (Global Interpreter Lock)
  • Es porque se interpreta y no se compila
  • Es porque es un lenguaje de tipado dinámico


Estan son las razones más escuchadas pero ¿Cuál es la que tiene mayor impacto?

Es el GIL (Global Interpreter Lock)

Las computadoras modernas vienen con CPU que tienen múltiples núcleos y, a veces, múltiples procesadores. Para utilizar toda esta potencia de procesamiento adicional, el sistema operativo define una estructura de bajo nivel llamada subproceso, donde un proceso (por ejemplo, el navegador Chrome) puede engendrar varios subprocesos y tener instrucciones para el sistema interno. De esta forma, si un proceso requiere una gran cantidad de CPU, esa carga se puede compartir entre los núcleos y esto hace que la mayoría de las aplicaciones.

Un concepto muy importante en este mundo es el de bloqueos. A diferencia de un proceso de subproceso único, debe asegurarse de que al cambiar variables en la memoria, varios subprocesos no intenten acceder  o cambiar la misma dirección de memoria al mismo tiempo.

Cuando CPython crea variables, asigna la memoria y luego cuenta cuántas referencias existen a esa variable, este es un concepto conocido como recuento de referencias. Si el número de referencias es 0, entonces libera esa porción de memoria del sistema. Esta es la razón por la cual la creación de una variable "temporal" dentro de, digamos, el alcance de un bucle for, no explota el consumo de memoria de su aplicación.

El desafío se convierte entonces cuando las variables se comparten en varios hilos, cómo CPython bloquea el recuento de referencias. Hay un "bloqueo de intérprete global" que controla cuidadosamente la ejecución de la secuencia. El intérprete solo puede ejecutar una operación a la vez, independientemente de la cantidad de subprocesos que tenga.

Si tiene una única aplicación de intérprete de subproceso único. No hará ninguna diferencia a la velocidad. Eliminar el GIL no tendría ningún impacto en el rendimiento de tu código.

Si desea implementar la concurrencia dentro de un único intérprete (proceso de Python) mediante el uso de subprocesos, y sus subprocesos son intensivos en IO (por ejemplo, IO de red o IO de disco), verá las consecuencias de la contención de GIL.

Si tiene una aplicación web (por ejemplo, Django) y está utilizando WSGI, entonces cada solicitud a su aplicación web es un intérprete independiente de Python, por lo que solo hay 1 bloqueo por solicitud. Debido a que el intérprete de Python tarda en iniciarse, algunas implementaciones de WSGI tienen un "Modo Daemon" que mantiene los procesos de Python sobre la marcha.

Es porque se interpreta y no se compila

Si decidis ejecutar un script en python, CPython iniciaría una larga secuencia de lecturas, análisis, compilación, interpretación y ejecución de ese código.

Un punto importante en ese proceso es la creación de un archivo .pyc, en la etapa de compilación, la secuencia de bytecode se escribe en un archivo dentro de __pycache __ / en Python 3 o en el mismo directorio en Python 2. Esto no solo se aplica al script, todo el código importado, incluidos los módulos de terceros.

Entonces, la mayoría de las veces (a menos que escriba código que solo ejecuta una vez), Python está interpretando bytecode y ejecutándolo localmente. Compare eso con Java o  .NET :
Java se compila en un "lenguaje intermedio" y la máquina virtual de Java lee el bytecode y lo compila en código de máquina. .NET CIL es el mismo, el .NET Common-Language-Runtime, CLR, utiliza compilación justo a tiempo para el código de la máquina.

Entonces, ¿por qué Python es mucho más lento que Java y C # en los benchmarks si todos usan una máquina virtual y algún tipo de Bytecode? En primer lugar, .NET y Java son JIT-Compiled.

La compilación JIT o Just-in-time requiere un lenguaje intermedio para permitir que el código se divida en fragmentos (o marcos). Los compiladores de anticipación (AOT) están diseñados para garantizar que la CPU pueda entender cada línea del código antes de que tenga lugar cualquier interacción.

El JIT en sí mismo no hace que la ejecución sea más rápida, porque todavía está ejecutando las mismas secuencias de código de bytes. Sin embargo, JIT permite realizar optimizaciones en tiempo de ejecución. Un buen optimizador de JIT verá qué partes de la aplicación se están ejecutando mucho, llame a estos "puntos calientes". Luego hará optimizaciones para esos bits de código, reemplazándolos con versiones más eficientes.

Esto significa que cuando la aplicación hace lo mismo una y otra vez, puede ser significativamente más rápido. Además, tenga en cuenta que Java y C# son lenguajes fuertemente tipados, por lo que el optimizador puede hacer muchas más suposiciones sobre el código.

PyPy tiene un JIT y es significativamente más rápido que CPython. 

Hay desventajas para los JIT, uno de ellos es el tiempo de inicio. PyPy es 2-3 veces más lento que CPython. La máquina virtual de Java es notoriamente lenta para arrancar. El .NET CLR soluciona esto comenzando en el arranque del sistema operativo, pero los desarrolladores del CLR también desarrollan el sistema operativo en el que se ejecuta el CLR.

Si tiene un único proceso de Python en ejecución durante mucho tiempo, con un código que puede optimizarse porque contiene "puntos calientes", entonces un JIT tiene mucho sentido.

Sin embargo, CPython es una implementación de propósito general. Entonces, si estuviera desarrollando aplicaciones de línea de comandos usando Python, tener que esperar que se inicie un JIT cada vez que se llamaba a la CLI sería terriblemente lento.

CPython tiene que tratar de servir tantos casos de uso como sea posible. Hubo la posibilidad de conectar un JIT en CPython, pero este proyecto se ha estancado en gran medida.

Es porque es un lenguaje de tipado dinámico

En un lenguaje "Estáticamente tipado", debe especificar el tipo de una variable cuando se declara. Esos incluirían C, C ++, Java, C #, Go.

En un lenguaje de tipado dinámico, todavía existe el concepto de tipos, pero el tipo de variable es dinámico. Por ejemplo: 

a = 1
a = "foo"

En este ejemplo, Python crea una segunda variable a de tipo de string y desasigna la memoria creada para la primera instancia.

No tener que declarar el tipo no es lo que hace que Python sea lento, el diseño del lenguaje Python le permite hacer casi cualquier cosa dinámica. Puede reemplazar los métodos en objetos en tiempo de ejecución, puede aplicar parche a las llamadas de bajo nivel del sistema operativo a un valor declarado en tiempo de ejecución. Casi todo es posible.

Es este diseño lo que hace que sea increíblemente difícil optimizar Python. Entonces, ¿la escritura dinámica de Python lo hace lento?

Comparar y convertir tipos es costoso, cada vez que se lee una variable, se escribe o se hace referencia a ella, se comprueba el tipo. Es difícil optimizar un lenguaje que es tan dinámico. La razón por la que muchas alternativas a Python son mucho más rápidas es que hacen concesiones a la flexibilidad en nombre del rendimiento. Observar a Cython, que combina C-Static Types y Python para optimizar el código donde se conocen los tipos, puede proporcionar una mejora de rendimiento.

Conclusión

Python es principalmente lento debido a su naturaleza dinámica y versatilidad. Se puede utilizar como una herramienta para todo tipo de problemas, donde las alternativas más optimizadas y más rápidas están probablemente disponibles.

Sin embargo, hay formas de optimizar sus aplicaciones de Python aprovechando la sincronización, entendiendo las herramientas de creación de perfiles y considerando el uso de múltiples intérpretes.

  • Para aplicaciones donde el tiempo de inicio no es importante y el código podría beneficiar un JIT, considere PyPy.
  • Para partes de su código donde el rendimiento es crítico y tiene más variables de tipo estático, considere usar Cython.

Dejo links:
https://pypy.org/
http://cython.org/
https://www.python.org/