Y a-t-il des limitations techniques ou des fonctionnalités de langage qui empêchent mon script Python d'être aussi rapide qu'un programme C ++ équivalent?


10

Je suis un utilisateur de longue date de Python. Il y a quelques années, j'ai commencé à apprendre le C ++ pour voir ce qu'il pouvait offrir en termes de vitesse. Pendant ce temps, je continuerais à utiliser Python comme outil de prototypage. C'était, semble-t-il, un bon système: développement agile avec Python, exécution rapide en C ++.

Récemment, j'utilise de plus en plus Python et j'apprends à éviter tous les pièges et anti-patterns que j'ai rapidement utilisé au cours de mes premières années avec le langage. Je crois comprendre que l'utilisation de certaines fonctionnalités (listes de compréhension, énumérations, etc.) peut augmenter les performances.

Mais y a-t-il des limitations techniques ou des fonctionnalités de langage qui empêchent mon script Python d'être aussi rapide qu'un programme C ++ équivalent?


2
Oui il peut. Voir PyPy pour l'état de l'art dans les compilateurs Python.
Greg Hewgill

5
Toutes les variables en python sont polymorphes, ce qui signifie que le type de la variable n'est connu qu'au moment de l'exécution. Si vous voyez (en supposant des entiers) x + y dans les langages de type C, ils font un ajout d'entier. En python, il y aura un interrupteur sur les types de variables sur x et y, puis la fonction d'addition appropriée est sélectionnée, puis il y aura une vérification de dépassement et puis il y aura l'addition. À moins que python n'apprenne la saisie statique, cette surcharge ne disparaîtra jamais.
nwp

1
@nwp Non, c'est facile, voir PyPy. Les problèmes les plus délicats, toujours ouverts, comprennent: comment surmonter la latence de démarrage des compilateurs JIT, comment éviter les allocations pour les graphiques d'objets à longue durée de vie compliqués, et comment faire bon usage du cache en général.

Réponses:


11

J'ai un peu heurté ce mur moi-même quand j'ai pris un travail de programmation Python à temps plein il y a quelques années. J'adore Python, vraiment, mais quand j'ai commencé à faire des réglages de performances, j'ai eu des chocs grossiers.

Les pythonistes stricts peuvent me corriger, mais voici les choses que j'ai trouvées, peintes en traits très larges.

  • L'utilisation de la mémoire Python est assez effrayante. Python représente tout comme un dict - ce qui est extrêmement puissant, mais a pour résultat que même les types de données simples sont gigantesques. Je me souviens que le caractère "a" prenait 28 octets de mémoire. Si vous utilisez des structures de Big Data en Python, assurez-vous de compter sur numpy ou scipy, car elles sont soutenues par une implémentation directe de tableau d'octets.

Cela a un impact sur les performances, car cela signifie qu'il existe des niveaux supplémentaires d'indirection au moment de l'exécution, en plus de surcharger d'énormes quantités de mémoire par rapport aux autres langues.

  • Python possède un verrou d'interpréteur global, ce qui signifie que, pour la plupart, les processus s'exécutent sur un seul thread. Il peut y avoir des bibliothèques qui répartissent les tâches entre les processus, mais nous faisions tourner environ 32 instances de notre script python et exécutions chaque thread unique.

D'autres peuvent parler du modèle d'exécution, mais Python est une compilation à l'exécution puis interprétée, ce qui signifie qu'il ne va pas jusqu'au code machine. Cela a également un impact sur les performances. Vous pouvez facilement créer des liens dans des modules C ou C ++, ou les trouver, mais si vous exécutez simplement Python, les performances seront affectées.

Désormais, dans les tests de performances de services Web, Python se compare favorablement aux autres langages de compilation à l'exécution comme Ruby ou PHP. Mais c'est assez loin derrière la plupart des langues compilées. Même les langages qui se compilent en langage intermédiaire et s'exécutent dans une machine virtuelle (comme Java ou C #) font beaucoup, beaucoup mieux.

Voici un ensemble très intéressant de tests de référence auxquels je me réfère occasionnellement:

http://www.techempower.com/benchmarks/

(Cela dit, j'aime toujours beaucoup Python, et si j'ai la chance de choisir la langue dans laquelle je travaille, c'est mon premier choix. La plupart du temps, je ne suis pas contraint par des exigences de débit folles de toute façon.)


2
La chaîne "a" n'est pas un bon exemple pour le premier point. Une chaîne Java a également un surcoût considérable pour les chaînes de caractères uniques, mais c'est un surcoût constant qui s'amortit assez bien à mesure que la chaîne grandit (un à quatre octets de caractères en fonction de la version, des options de construction et du contenu de la chaîne). Cependant, vous avez raison sur les objets définis par l'utilisateur, du moins ceux qui n'utilisent pas __slots__. PyPy devrait faire beaucoup mieux à cet égard, mais je n'en sais pas assez pour en juger.

1
Le deuxième problème que vous signalez n'est lié qu'à une implémentation spécifique et n'est pas inhérent au langage. Le premier problème nécessite une explication: ce qui "pèse" 28 octets n'est pas le caractère lui-même mais le fait qu'il a été emballé dans une classe de chaîne, avec ses propres méthodes et propriétés. Représenter un seul caractère sous la forme d'un tableau d'octets (littéral b'a ') "seulement" pèse 18 octets sur Python 3.3 et je suis sûr qu'il existe d'autres façons d'optimiser le stockage des caractères en mémoire si votre application en a vraiment besoin.
Rouge

C # peut compiler nativement (par exemple, la prochaine technologie MS, Xamarin pour iOS).
Den

13

L'implémentation de référence Python est l'interpréteur «CPython». Il essaie d'être relativement rapide, mais il n'utilise pas actuellement d'optimisations avancées. Et pour de nombreux scénarios d'utilisation, c'est une bonne chose: la compilation vers un code intermédiaire se produit immédiatement avant l'exécution, et à chaque exécution du programme, le code est à nouveau compilé. Le temps nécessaire à l'optimisation doit donc être mis en balance avec le temps gagné par les optimisations - s'il n'y a pas de gain net, l'optimisation est sans valeur. Pour un programme très long ou un programme avec des boucles très serrées, l'utilisation d'optimisations avancées serait utile. Cependant, CPython est utilisé pour certains travaux qui empêchent une optimisation agressive:

  • Scripts de courte durée, utilisés par exemple pour les tâches sysadmin. De nombreux systèmes d'exploitation comme Ubuntu construisent une bonne partie de leur infrastructure au-dessus de Python: CPython est assez rapide pour le travail, mais n'a pratiquement pas de temps de démarrage. Tant que c'est plus rapide que bash, c'est bon.

  • CPython doit avoir une sémantique claire, car il s'agit d'une implémentation de référence. Cela permet des optimisations simples telles que «optimiser la mise en œuvre de l'opérateur foo» ou «compiler les compréhensions de listes pour accélérer le bytecode», mais exclura généralement les optimisations qui détruisent les informations, telles que les fonctions en ligne.

Bien sûr, il y a plus d'implémentations Python que juste CPython:

  • Jython est construit au-dessus de la JVM. La machine virtuelle Java peut interpréter ou compiler JIT le bytecode fourni et dispose d'optimisations guidées par profil. Il souffre d'un temps de démarrage élevé et il faut du temps pour que le JIT entre en action.

  • PyPy est un état de l'art, JITting Python VM. PyPy est écrit en RPython, un sous-ensemble restreint de Python. Ce sous-ensemble supprime une certaine expressivité de Python, mais permet de déduire statiquement le type de n'importe quelle variable. La machine virtuelle écrite en RPython peut ensuite être transposée en C, ce qui donne des performances similaires à RPython C. Cependant, RPython est toujours plus expressif que C, ce qui permet un développement plus rapide de nouvelles optimisations. PyPy est un exemple d'amorçage du compilateur. PyPy (pas RPython!) Est principalement compatible avec l'implémentation de référence CPython.

  • Cython est (comme RPython) un dialecte Python incompatible avec le typage statique. Il transpile également en code C et est capable de générer facilement des extensions C pour l'interpréteur CPython.

Si vous êtes prêt à traduire votre code Python en Cython ou RPython, vous obtiendrez des performances de type C. Cependant, ils ne doivent pas être compris comme «un sous-ensemble de Python», mais plutôt comme «C avec la syntaxe Pythonic». Si vous passez à PyPy, votre code Python vanille obtiendra une augmentation de vitesse considérable, mais ne pourra pas non plus s'interfacer avec les extensions écrites en C ou C ++.

Mais quelles propriétés ou fonctionnalités empêchent vanilla Python d'atteindre des niveaux de performances de type C, en dehors des longs temps de démarrage?

  • Contributeurs et financement. Contrairement à Java ou C #, il n'y a pas une seule entreprise motrice derrière le langage qui souhaite faire de ce langage le meilleur de sa catégorie. Cela limite le développement aux bénévoles et aux subventions occasionnelles.

  • Reliure tardive et absence de frappe statique. Python nous permet d'écrire de la merde comme ceci:

    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, n'importe quelle variable peut être réaffectée à tout moment. Cela empêche la mise en cache ou l'inline; tout accès doit passer par la variable. Cette indirection alourdit les performances. Bien sûr: si votre code ne fait pas des choses aussi folles pour que chaque variable puisse recevoir un type définitif avant la compilation et que chaque variable ne soit affectée qu'une seule fois, alors - en théorie - un modèle d'exécution plus efficace pourrait être choisi. Un langage dans ce sens fournirait un moyen de marquer les identifiants comme constantes et permettrait au moins des annotations de type facultatives («typage progressif»).

  • Un modèle d'objet discutable. À moins que des emplacements ne soient utilisés, il est difficile de déterminer les champs d'un objet (un objet Python est essentiellement une table de hachage de champs). Et même une fois que nous y sommes, nous n'avons toujours aucune idée des types de ces champs. Cela empêche de représenter des objets sous forme de structures très compactes, comme c'est le cas en C ++. (Bien sûr, la représentation d'objets C ++ n'est pas idéale non plus: en raison de la nature structurelle, même les champs privés appartiennent à l'interface publique d'un objet.)

  • Collecte des ordures. Dans de nombreux cas, la GC pourrait être évitée complètement. C ++ permet d'allouer statiquement des objets qui sont détruits automatiquement lorsque la portée actuelle reste: Type instance(args);. Jusque-là, l'objet est vivant et peut être prêté à d'autres fonctions. Cela se fait généralement par «passe-par-référence». Des langages comme Rust permettent au compilateur de vérifier statiquement qu'aucun pointeur vers un tel objet ne dépasse la durée de vie de l'objet. Ce schéma de gestion de la mémoire est totalement prévisible, très efficace et convient à la plupart des cas sans graphiques d'objets compliqués. Malheureusement, Python n'a pas été conçu avec la gestion de la mémoire à l'esprit. En théorie, l'analyse d'échappement peut être utilisée pour trouver des cas où la GC peut être évitée. En pratique, des chaînes de méthodes simples telles quefoo().bar().baz() devra allouer un grand nombre d'objets de courte durée de vie sur le tas (GC générationnel est une façon de garder ce problème petit).

    Dans d'autres cas, le programmeur peut déjà connaître la taille finale d'un objet tel qu'une liste. Malheureusement, Python n'offre pas de moyen de communiquer cela lors de la création d'une nouvelle liste. Au lieu de cela, les nouveaux éléments seront poussés à la fin, ce qui pourrait nécessiter plusieurs réallocations. Quelques notes:

    • Des listes d'une taille spécifique peuvent être créées comme fixed_size = [None] * size. Cependant, la mémoire des objets de cette liste devra être allouée séparément. Contraste C ++, où nous pouvons le faire std::array<Type, size> fixed_size.

    • Des tableaux compressés d'un type natif spécifique peuvent être créés en Python via le arraymodule intégré. En outre, numpyoffre des représentations efficaces de tampons de données avec des formes spécifiques pour les types numériques natifs.

Sommaire

Python a été conçu pour être facile à utiliser, pas pour les performances. Sa conception rend la création d'une mise en œuvre hautement efficace assez difficile. Si le programmeur s'abstient de fonctionnalités problématiques, un compilateur comprenant les idiomes restants sera en mesure d'émettre du code efficace qui peut rivaliser avec C en termes de performances.


8

Oui. Le principal problème est que le langage est défini comme dynamique, c'est-à-dire que vous ne savez jamais ce que vous faites tant que vous n'êtes pas sur le point de le faire. Cela rend très difficile de produire du code efficace de la machine, parce que vous ne savez pas quoi produire du code machine pour . Les compilateurs JIT peuvent faire un peu de travail dans ce domaine, mais ce n'est jamais comparable au C ++ parce que le compilateur JIT ne peut tout simplement pas passer du temps et de la mémoire à fonctionner, car c'est du temps et de la mémoire que vous ne dépensez pas pour exécuter votre programme, et il y a des limites strictes sur ce ils peuvent atteindre sans briser la sémantique du langage dynamique.

Je ne vais pas prétendre que c'est un compromis inacceptable. Mais il est fondamental pour la nature de Python que les implémentations réelles ne soient jamais aussi rapides que les implémentations C ++.


8

Il existe trois principaux facteurs qui affectent les performances de toutes les langues dynamiques, certains plus que d'autres.

  1. Frais généraux d'interprétation. Au moment de l'exécution, il existe une sorte de code d'octet plutôt que des instructions machine et il y a une surcharge fixe pour exécuter ce code.
  2. Expédiez les frais généraux. La cible d'un appel de fonction n'est pas connue avant l'exécution et la recherche de la méthode à appeler a un coût.
  3. Frais généraux de gestion de la mémoire. Les langages dynamiques stockent des éléments dans des objets qui doivent être alloués et désalloués, et qui entraînent une surcharge de performances.

Pour C / C ++, les coûts relatifs de ces 3 facteurs sont presque nuls. Les instructions sont exécutées directement par le processeur, la répartition prend au plus une ou deux indirection, la mémoire de tas n'est jamais allouée sauf si vous le dites. Un code bien écrit peut approcher le langage d'assemblage.

Pour la compilation C # / Java avec JIT, les deux premiers sont faibles, mais la mémoire récupérée a un coût. Un code bien écrit peut approcher 2x C / C ++.

Pour Python / Ruby / Perl, le coût de ces trois facteurs est relativement élevé. Pensez 5 fois par rapport à C / C ++ ou pire. (*)

N'oubliez pas que le code de la bibliothèque d'exécution peut très bien être écrit dans le même langage que vos programmes et avoir les mêmes limitations de performances.


(*) Comme la compilation Just-In_Time (JIT) est étendue à ces langages, ils approcheront aussi (généralement 2x) la vitesse d'un code C / C ++ bien écrit.

Il convient également de noter qu'une fois que l'écart est étroit (entre les langues concurrentes), les différences sont dominées par les algorithmes et les détails de mise en œuvre. Le code JIT peut battre C / C ++ et C / C ++ peut battre le langage assembleur car il est juste plus facile d'écrire du bon code.


"N'oubliez pas que le code de la bibliothèque d'exécution peut très bien être écrit dans le même langage que vos programmes et avoir les mêmes limitations de performances." et "Pour Python / Ruby / Perl, le coût de ces trois facteurs est relativement élevé. Pensez 5 fois par rapport à C / C ++ ou pire." En fait, ce n'est pas vrai. Par exemple, la Hashclasse Rubinius (l'une des infrastructures de données de base de Ruby) est écrite en Ruby, et elle fonctionne de manière comparable, parfois même plus rapide, que la Hashclasse de YARV qui est écrite en C. Et l'une des raisons est que de grandes parties de l'exécution de Rubinius système sont écrits en Ruby, pour qu'ils puissent…
Jörg W Mittag

… Par exemple être intégré par le compilateur Rubinius. Des exemples extrêmes sont la machine virtuelle Klein (une machine virtuelle métacirculaire pour Self) et la machine virtuelle Maxine (une machine virtuelle métacirculaire pour Java), où tout , même le code de répartition de la méthode, le garbage collector, l'allocateur de mémoire, les types primitifs, les infrastructures de données de base et les algorithmes sont écrits en Soi ou Java. De cette façon, même des parties de la machine virtuelle principale peuvent être insérées dans le code utilisateur, et la machine virtuelle peut recompiler et se réoptimiser à l'aide des commentaires d'exécution du programme utilisateur.
Jörg W Mittag

@ JörgWMittag: Toujours vrai. Rubinius a JIT, et le code JIT bat souvent C / C ++ sur les benchmarks individuels. Je ne trouve aucune preuve que ce truc métacirculaire fait beaucoup pour la vitesse en l'absence de JIT. [Voir la modification pour plus de clarté sur JIT.]
david.pfx

1

Mais y a-t-il des limitations techniques ou des fonctionnalités de langage qui empêchent mon script Python d'être aussi rapide qu'un programme C ++ équivalent?

Non. C'est juste une question d'argent et de ressources consacrées à faire fonctionner C ++ rapidement par rapport à l'argent et aux ressources investies pour faire fonctionner Python rapidement.

Par exemple, lorsque le Self VM est sorti, ce n'était pas seulement le langage OO dynamique le plus rapide, c'était la période de langage OO la plus rapide. Bien qu'il s'agisse d'un langage incroyablement dynamique (beaucoup plus que Python, Ruby, PHP ou JavaScript, par exemple), il était plus rapide que la plupart des implémentations C ++ disponibles.

Mais Sun a annulé le projet Self (un langage OO à usage général mature pour développer de grands systèmes) pour se concentrer sur un petit langage de script pour les menus animés dans les décodeurs TV (vous en avez peut-être entendu parler, il s'appelle Java), il n'y avait pas plus de financement. Parallèlement, Intel, IBM, Microsoft, Sun, Metrowerks, HP et al. dépensé de grandes quantités d'argent et de ressources pour accélérer le C ++. Les fabricants de CPU ont ajouté des fonctionnalités à leurs puces pour rendre C ++ rapide. Les systèmes d'exploitation ont été écrits ou modifiés pour rendre C ++ rapide. Donc, C ++ est rapide.

Je ne connais pas très bien Python, je suis plus une personne Ruby, donc je vais donner un exemple de Ruby: la Hashclasse (équivalente en fonction et en importance dicten Python) dans l'implémentation Rubinius Ruby est écrite en Ruby 100% pur; Pourtant, il rivalise favorablement et parfois même surpasse la Hashclasse dans YARV qui est écrit en C. optimisé à la main. Et comparé à certains des systèmes commerciaux Lisp ou Smalltalk (ou à la Self VM susmentionnée), le compilateur de Rubinius n'est même pas si intelligent .

Il n'y a rien d'inhérent à Python qui le rend lent. Il y a des fonctionnalités dans les processeurs et les systèmes d'exploitation d'aujourd'hui qui nuisent à Python (par exemple, la mémoire virtuelle est connue pour être terrible pour les performances de récupération de place). Il existe des fonctionnalités qui aident C ++ mais n'aident pas Python (les processeurs modernes essaient d'éviter les échecs de cache, car ils sont si chers. Malheureusement, éviter les échecs de cache est difficile lorsque vous avez OO et le polymorphisme. Vous devriez plutôt réduire le coût du cache Le processeur Azul Vega, conçu pour Java, le fait.)

Si vous dépensez autant d'argent, de recherches et de ressources pour rendre Python rapide, comme cela a été fait pour C ++, et que vous dépensez autant d'argent, de recherches et de ressources pour créer des systèmes d'exploitation qui permettent aux programmes Python de s'exécuter rapidement comme cela a été fait pour C ++ et vous dépensez autant beaucoup d'argent, de recherche et de ressources pour faire des processeurs qui font que les programmes Python s'exécutent rapidement comme cela a été fait pour C ++, alors il ne fait aucun doute dans mon esprit que Python pourrait atteindre des performances comparables à C ++.

Nous avons vu avec ECMAScript ce qui peut arriver si un seul joueur prend au sérieux les performances. En un an, nous avons pratiquement augmenté de 10 fois les performances de tous les principaux fournisseurs.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.