¿Existen limitaciones técnicas o características del lenguaje que impiden que mi script Python sea tan rápido como un programa C ++ equivalente?

10

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?

KidElephant
fuente
2
Sí puede. Vea PyPy para conocer el estado del arte en los compiladores de Python.
Greg Hewgill
55
Todas las variables en python son polimórficas, lo que significa que el tipo de la variable solo se conoce en tiempo de ejecución. Si ve (suponiendo enteros) x + y en lenguajes tipo C, hacen una suma de enteros. En python habrá un interruptor en los tipos de variables en x e y luego se seleccionará la función de adición apropiada y luego habrá una verificación de desbordamiento y luego habrá la adición. A menos que Python aprenda la escritura estática, esta sobrecarga nunca desaparecerá.
nwp
1
@nwp No, eso es fácil, mira PyPy. Los problemas más complicados, aún abiertos, incluyen: cómo superar la latencia de inicio de los compiladores JIT, cómo evitar asignaciones para gráficos de objetos complicados de larga duración y cómo hacer un buen uso de la memoria caché en general.

Respuestas:

11

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.

  • El uso de la memoria Python es un poco aterrador. Python representa todo como un dict, que es extremadamente poderoso, pero tiene como resultado que incluso los tipos de datos simples son gigantes. Recuerdo que el carácter "a" tomó 28 bytes de memoria. Si está utilizando estructuras de big data en Python, asegúrese de confiar en numpy o scipy, porque están respaldadas por una implementación directa de matriz de bytes.

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.

  • Python tiene un bloqueo global de intérprete, lo que significa que, en su mayor parte, los procesos se ejecutan con un solo subproceso. Puede haber bibliotecas que distribuyen tareas entre los procesos, pero estábamos generando 32 instancias de nuestro script de Python y ejecutando cada subproceso.

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).

Robar
fuente
2
La cadena "a" no es un buen ejemplo para el primer punto de viñeta. Una cadena de Java también tiene una sobrecarga considerable para las cadenas de un solo carácter, pero es una sobrecarga constante que se amortiza bastante bien a medida que la cadena crece en longitud (de uno a cuatro bytes de caracteres dependiendo de la versión, las opciones de compilación y el contenido de la cadena). Sin embargo, tienes razón sobre los objetos definidos por el usuario, al menos los que no usan __slots__. PyPy debería hacerlo mucho mejor a este respecto, pero no sé lo suficiente para juzgar.
1
El segundo problema que está señalando está relacionado solo con una implementación específica y no es inherente al lenguaje. El primer problema requiere explicación: lo que "pesa" 28 bytes no es el carácter en sí, sino el hecho de que se ha empaquetado en una clase de cadena, que viene con sus propios métodos y propiedades. La representación de un solo carácter como matriz de bytes (literal b'a ') "solo" pesa 18 bytes en Python 3.3 y estoy seguro de que hay más formas de optimizar el almacenamiento de caracteres en la memoria si su aplicación realmente lo necesita.
Rojo
C # puede compilar de forma nativa (por ejemplo, próxima tecnología de MS, Xamarin para iOS).
Den
13

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í:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding

    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 hacer std::array<Type, size> fixed_size.

    • Las matrices empaquetadas de un tipo nativo específico se pueden crear en Python a través del arraymódulo incorporado. Además, numpyofrece 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.

amon
fuente
8

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 ++.

DeadMG
fuente
8

Hay tres factores principales que afectan el rendimiento de todos los lenguajes dinámicos, algunos más que otros.

  1. Sobrecarga interpretativa. En tiempo de ejecución hay algún tipo de código de bytes en lugar de instrucciones de máquina y hay una sobrecarga fija para ejecutar este código.
  2. Despacho por encima. El objetivo de una llamada de función no se conoce hasta el tiempo de ejecución, y descubrir qué método llamar conlleva un costo.
  3. Gestión de memoria de gastos generales. Los lenguajes dinámicos almacenan cosas en objetos que deben asignarse y desasignarse, y eso conlleva una sobrecarga de rendimiento.

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.

david.pfx
fuente
"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". y "Para Python / Ruby / Perl, el costo de estos tres factores es relativamente alto. Piense 5 veces en comparación con C / C ++ o peor". En realidad, eso no es cierto. Por ejemplo, la Hashclase 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 la Hashclase 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 ...
Jörg W Mittag
… Por ejemplo estar en línea por el compilador Rubinius. Ejemplos extremos son la VM Klein (una VM metacircular para Self) y la VM Maxine (una VM metacircular para Java), donde todo , incluso el código de envío del método, el recolector de basura, el asignador de memoria, los tipos primitivos, las estructuras de datos centrales y los algoritmos están escritos en Self o Java. De esa manera, incluso partes de la VM central pueden integrarse en el código de usuario, y la VM puede volver a compilarse y optimizarse utilizando la retroalimentación de tiempo de ejecución del programa de usuario.
Jörg W Mittag
@ JörgWMittag: sigue siendo cierto. Rubinius tiene JIT, y el código JIT a menudo supera a C / C ++ en puntos de referencia individuales. No puedo encontrar ninguna evidencia de que este material metacircular haga mucho por la velocidad en ausencia de JIT. [Ver edición para mayor claridad sobre JIT.]
david.pfx
1

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?

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 Hashclase (equivalente en función e importancia a dictPython) en la implementación de Rubinius Ruby está escrita en Ruby 100% puro; Sin embargo, compite favorablemente y, a veces, incluso supera a la Hashclase 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.

Jörg W Mittag
fuente