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?
la source
Réponses:
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.
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.
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.)
la source
__slots__
. PyPy devrait faire beaucoup mieux à cet égard, mais je n'en sais pas assez pour en juger.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:
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 fairestd::array<Type, size> fixed_size
.Des tableaux compressés d'un type natif spécifique peuvent être créés en Python via le
array
module intégré. En outre,numpy
offre 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.
la source
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 ++.
la source
Il existe trois principaux facteurs qui affectent les performances de toutes les langues dynamiques, certains plus que d'autres.
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.
la source
Hash
classe 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 laHash
classe 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…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
Hash
classe (équivalente en fonction et en importancedict
en Python) dans l'implémentation Rubinius Ruby est écrite en Ruby 100% pur; Pourtant, il rivalise favorablement et parfois même surpasse laHash
classe 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.
la source