Soy un usuario de Python desde hace mucho tiempo. Hace unos años, comencé a aprender C ++ para ver qué podía ofrecer en términos de velocidad. Durante este tiempo, seguiría usando Python como herramienta para la creación de prototipos. Al parecer, este era un buen sistema: desarrollo ágil con Python, ejecución rápida en C ++.
Recientemente, he estado usando Python una y otra vez, y aprendí a evitar todas las trampas y antipatrones que utilicé rápidamente en mis primeros años con el lenguaje. Entiendo que el uso de ciertas funciones (comprensión de listas, enumeraciones, etc.) puede aumentar el rendimiento.
Pero, ¿existen limitaciones técnicas o características del lenguaje que impiden que mi script Python sea tan rápido como un programa C ++ equivalente?
fuente
Respuestas:
Me golpeé contra este muro cuando tomé un trabajo de programación de Python a tiempo completo hace un par de años. Me encanta Python, de verdad, pero cuando comencé a hacer algunos ajustes de rendimiento, tuve algunos golpes groseros.
Los Pythonistas estrictos pueden corregirme, pero aquí están las cosas que encontré, pintadas con trazos muy amplios.
Eso tiene un impacto en el rendimiento, ya que significa que hay niveles adicionales de indirección en el tiempo de ejecución, además de generar grandes cantidades de memoria en comparación con otros idiomas.
Otros pueden hablar con el modelo de ejecución, pero Python es una compilación en tiempo de ejecución y luego se interpreta, lo que significa que no llega hasta el código de la máquina. Eso también tiene un impacto en el rendimiento. Puede vincular fácilmente en módulos C o C ++, o encontrarlos, pero si solo ejecuta Python directamente, tendrá un impacto en el rendimiento.
Ahora, en los puntos de referencia del servicio web, Python se compara favorablemente con otros lenguajes de compilación en tiempo de ejecución como Ruby o PHP. Pero está bastante lejos de la mayoría de los idiomas compilados. Incluso los lenguajes que se compilan en lenguaje intermedio y se ejecutan en una máquina virtual (como Java o C #) funcionan mucho, mucho mejor.
Aquí hay un conjunto realmente interesante de pruebas de referencia a las que me refiero ocasionalmente:
http://www.techempower.com/benchmarks/
(Dicho todo esto, todavía me encanta Python, y si tengo la oportunidad de elegir el idioma en el que estoy trabajando, es mi primera opción. La mayoría de las veces, de todos modos, no estoy limitado por los requisitos de rendimiento loco).
fuente
__slots__
. PyPy debería hacerlo mucho mejor a este respecto, pero no sé lo suficiente para juzgar.La implementación de referencia de Python es el intérprete "CPython". Intenta ser razonablemente rápido, pero actualmente no emplea optimizaciones avanzadas. Y para muchos escenarios de uso, esto es algo bueno: la compilación de algún código intermediario ocurre inmediatamente antes del tiempo de ejecución, y cada vez que se ejecuta el programa, el código se compila nuevamente. Por lo tanto, el tiempo necesario para la optimización debe compararse con el tiempo ganado por las optimizaciones: si no hay una ganancia neta, la optimización no tiene valor. Para un programa de larga duración, o un programa con bucles muy ajustados, sería útil emplear optimizaciones avanzadas. Sin embargo, CPython se usa para algunos trabajos que impiden la optimización agresiva:
Scripts de ejecución corta, utilizados, por ejemplo, para tareas de administrador del sistema. Muchos sistemas operativos como Ubuntu construyen una buena parte de su infraestructura sobre Python: CPython es lo suficientemente rápido para el trabajo, pero prácticamente no tiene tiempo de arranque. Mientras sea más rápido que bash, es bueno.
CPython debe tener una semántica clara, ya que es una implementación de referencia. Esto permite optimizaciones simples como "optimizar la implementación del operador foo" o "compilar listas de comprensión para un bytecode más rápido", pero generalmente impedirá optimizaciones que destruyen información, como las funciones en línea.
Por supuesto, hay más implementaciones de Python que solo CPython:
Jython está construido sobre la JVM. El JVM puede interpretar o compilar JIT el bytecode proporcionado y tiene optimizaciones guiadas por perfil. Sufre de un alto tiempo de arranque y lleva un tiempo hasta que el JIT se activa.
PyPy es un estado del arte, JITting Python VM. PyPy está escrito en RPython, un subconjunto restringido de Python. Este subconjunto elimina algo de expresividad de Python, pero permite inferir estáticamente el tipo de cualquier variable. La VM escrita en RPython se puede transpilar a C, lo que proporciona un rendimiento similar a RPython C. Sin embargo, RPython es aún más expresivo que C, lo que permite un desarrollo más rápido de nuevas optimizaciones. PyPy es un ejemplo de arranque del compilador. PyPy (¡no RPython!) Es principalmente compatible con la implementación de referencia de CPython.
Cython es (como RPython) un dialecto de Python incompatible con escritura estática. También se transpira a código C y puede generar fácilmente extensiones C para el intérprete CPython.
Si está dispuesto a traducir su código de Python a Cython o RPython, obtendrá un rendimiento similar a C. Sin embargo, no deben entenderse como "un subconjunto de Python", sino más bien como "C con sintaxis pitónica". Si cambia a PyPy, su código de Python vainilla obtendrá un aumento considerable de la velocidad, pero tampoco podrá interactuar con extensiones escritas en C o C ++.
Pero, ¿qué propiedades o características evitan que Python vainilla alcance niveles de rendimiento tipo C, aparte de los largos tiempos de arranque?
Contribuyentes y financiación. A diferencia de Java o C #, no hay una sola compañía de manejo detrás del lenguaje con interés en hacer de este lenguaje el mejor de su clase. Esto restringe el desarrollo principalmente a voluntarios y subvenciones ocasionales.
Enlace tardío y la falta de cualquier tipo de escritura estática. Python nos permite escribir basura así:
En Python, cualquier variable se puede reasignar en cualquier momento. Esto evita el almacenamiento en caché o en línea; cualquier acceso tiene que pasar por la variable. Esta indirección socava el rendimiento. Por supuesto: si su código no hace cosas tan locas para que cada variable pueda recibir un tipo definitivo antes de la compilación y cada variable se asigne solo una vez, entonces, en teoría, podría elegirse un modelo de ejecución más eficiente. Un lenguaje con esto en mente proporcionaría alguna forma de marcar identificadores como constantes, y al menos permitiría anotaciones de tipo opcionales ("escritura gradual").
Un modelo de objeto cuestionable. A menos que se usen ranuras, es difícil determinar qué campos tiene un objeto (un objeto Python es esencialmente una tabla hash de campos). E incluso una vez que estamos allí, todavía no tenemos idea de qué tipos tienen estos campos. Esto evita representar objetos como estructuras compactas, como es el caso en C ++. (Por supuesto, la representación de objetos en C ++ tampoco es ideal: debido a la naturaleza de estructura, incluso los campos privados pertenecen a la interfaz pública de un objeto).
Recolección de basura. En muchos casos, GC podría evitarse por completo. C ++ nos permite asignar estáticamente los objetos que se destruyen automáticamente cuando el ámbito actual se deja:
Type instance(args);
. Hasta entonces, el objeto está vivo y se puede prestar a otras funciones. Esto generalmente se hace a través de "paso por referencia". Lenguajes como Rust permiten al compilador verificar estáticamente que ningún puntero a dicho objeto exceda la vida útil del objeto. Este esquema de administración de memoria es totalmente predecible, altamente eficiente y se adapta a la mayoría de los casos sin gráficos de objetos complicados. Desafortunadamente, Python no fue diseñado con la administración de memoria en mente. En teoría, el análisis de escape se puede utilizar para encontrar casos en los que se puede evitar la GC. En la práctica, cadenas de métodos simples comofoo().bar().baz()
tendrá que asignar una gran cantidad de objetos de corta duración en el montón (el GC generacional es una forma de mantener este problema pequeño).En otros casos, el programador ya puede conocer el tamaño final de algún objeto, como una lista. Desafortunadamente, Python no ofrece una manera de comunicar esto al crear una nueva lista. En cambio, los nuevos elementos serán empujados al final, lo que puede requerir múltiples reasignaciones. Algunas notas
Se pueden crear listas de un tamaño específico
fixed_size = [None] * size
. Sin embargo, la memoria para los objetos dentro de esa lista tendrá que asignarse por separado. Contraste C ++, donde podemos hacerstd::array<Type, size> fixed_size
.Las matrices empaquetadas de un tipo nativo específico se pueden crear en Python a través del
array
módulo incorporado. Además,numpy
ofrece representaciones eficientes de memorias intermedias de datos con formas específicas para tipos numéricos nativos.Resumen
Python fue diseñado para facilitar su uso, no para el rendimiento. Su diseño hace que crear una implementación altamente eficiente sea bastante difícil. Si el programador se abstiene de características problemáticas, entonces un compilador que comprenda los modismos restantes podrá emitir un código eficiente que puede rivalizar con C en el rendimiento.
fuente
Si. El problema principal es que el lenguaje se define como dinámico, es decir, nunca sabes lo que estás haciendo hasta que estás a punto de hacerlo. Eso hace que sea muy difícil de producir el código máquina eficiente, ya que no sabe qué código de máquina productos para . Los compiladores JIT pueden hacer algo de trabajo en esta área, pero nunca es comparable a C ++ porque el compilador JIT simplemente no puede gastar tiempo y memoria en ejecución, ya que ese es tiempo y memoria que no está gastando en ejecutar su programa, y hay límites duros sobre lo que pueden lograr sin romper la semántica dinámica del lenguaje.
No voy a afirmar que esta es una compensación inaceptable. Pero es fundamental para la naturaleza de Python que las implementaciones reales nunca serán tan rápidas como las implementaciones de C ++.
fuente
Hay tres factores principales que afectan el rendimiento de todos los lenguajes dinámicos, algunos más que otros.
Para C / C ++, los costos relativos de estos 3 factores son casi cero. Las instrucciones son ejecutadas directamente por el procesador, el despacho toma como máximo una o dos indirectas, la memoria del montón nunca se asigna a menos que usted lo indique. El código bien escrito puede acercarse al lenguaje ensamblador.
Para C # / Java con compilación JIT, los dos primeros son bajos, pero la memoria recolectada de basura tiene un costo. El código bien escrito puede acercarse a 2x C / C ++.
Para Python / Ruby / Perl, el costo de estos tres factores es relativamente alto. Piensa 5 veces en comparación con C / C ++ o peor. (*)
Recuerde que el código de la biblioteca en tiempo de ejecución puede estar escrito en el mismo idioma que sus programas y tener las mismas limitaciones de rendimiento.
(*) A medida que la compilación Just-In_Time (JIT) se extienda a estos lenguajes, también se acercarán (normalmente 2x) a la velocidad del código C / C ++ bien escrito.
También se debe tener en cuenta que una vez que la brecha es estrecha (entre los idiomas de la competencia), las diferencias están dominadas por los algoritmos y los detalles de implementación. El código JIT puede vencer a C / C ++ y C / C ++ puede vencer al lenguaje ensamblador porque es más fácil escribir un buen código.
fuente
Hash
clase Rubinius (una de las estructuras de datos centrales en Ruby) está escrita en Ruby, y tiene un rendimiento comparable, a veces incluso más rápido, que laHash
clase de YARV que está escrita en C. Y una de las razones es que gran parte del tiempo de ejecución de Rubinius sistema están escritos en Ruby, para que puedan ...No. Es solo una cuestión de dinero y recursos invertidos en hacer que C ++ se ejecute rápido versus dinero y recursos invertidos en hacer que Python se ejecute rápido.
Por ejemplo, cuando salió la Self VM, no solo era el lenguaje OO dinámico más rápido, sino también el período de lenguaje OO más rápido. A pesar de ser un lenguaje increíblemente dinámico (mucho más que Python, Ruby, PHP o JavaScript, por ejemplo), fue más rápido que la mayoría de las implementaciones de C ++ que estaban disponibles.
Pero luego Sun canceló el proyecto Self (un lenguaje OO de propósito general maduro para desarrollar sistemas grandes) para enfocarse en un pequeño lenguaje de scripting para menús animados en decodificadores de TV (es posible que haya oído hablar de eso, se llama Java), no hubo Más financiación. Al mismo tiempo, Intel, IBM, Microsoft, Sun, Metrowerks, HP et al. gastó grandes cantidades de dinero y recursos haciendo que C ++ sea rápido. Los fabricantes de CPU agregaron características a sus chips para hacer que C ++ sea rápido. Los sistemas operativos fueron escritos o modificados para hacer que C ++ sea rápido. Entonces, C ++ es rápido.
No estoy terriblemente familiarizado con Python, soy más una persona Ruby, así que daré un ejemplo de Ruby: la
Hash
clase (equivalente en función e importancia adict
Python) en la implementación de Rubinius Ruby está escrita en Ruby 100% puro; Sin embargo, compite favorablemente y, a veces, incluso supera a laHash
clase en YARV que está escrita en C. optimizada a mano. Y en comparación con algunos de los sistemas comerciales Lisp o Smalltalk (o el mencionado Self VM), el compilador de Rubinius ni siquiera es tan inteligente .No hay nada inherente en Python que lo haga lento. Hay características en los procesadores y sistemas operativos actuales que perjudican a Python (por ejemplo, se sabe que la memoria virtual es terrible para el rendimiento de la recolección de basura). Hay características que ayudan a C ++ pero no ayudan a Python (las CPU modernas intentan evitar errores de caché, porque son muy caros. Desafortunadamente, evitar errores de caché es difícil cuando tienes OO y polimorfismo. Por el contrario, debes reducir el costo del caché El CPU Azul Vega, que fue diseñado para Java, hace esto.
Si gasta tanto dinero, investigación y recursos para hacer que Python sea rápido, como se hizo para C ++, y gasta tanto dinero, investigación y recursos para hacer que los sistemas operativos que hacen que los programas de Python se ejecuten rápido como lo hizo para C ++ y gaste como mucho dinero, investigación y recursos para hacer CPU que hacen que los programas de Python se ejecuten rápidamente como se hizo con C ++, entonces no tengo dudas de que Python podría alcanzar un rendimiento comparable al de C ++.
Hemos visto con ECMAScript lo que puede suceder si solo un jugador toma en serio el rendimiento. Dentro de un año, tuvimos básicamente un aumento del rendimiento de 10 veces en todos los ámbitos para todos los principales proveedores.
fuente