Y a-t-il une raison pour laquelle Python 3 énumère plus lentement que Python 2?

Python 3 semble être plus lent dans les énumérations pour une boucle minimum que Python 2 par une marge significative, ce qui semble s'aggraver avec les nouvelles versions de Python 3.

J'ai Python 2.7.6, Python 3.3.3, et Python 3.4.0 installé sur ma machine windows 64 bits, (Intel i7-2700K-3.5 GHz) avec les versions 32 bits et 64 bits de chaque Python installé. Bien qu'il n'y ait pas de différence significative dans la vitesse d'exécution entre 32 bits et 64 bits pour une donnée version dans ses limites quant à l'accès mémoire, il y a une différence très significative entre les différents niveaux de version. Je vais laisser les résultats du timing parler pour eux-mêmes comme suit:

C:\**Python34_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **900 msec** per loop

C:\**Python33_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **820 msec** per loop

C:\**Python27_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **480 msec** per loop

puisque la "gamme" de Python 3 n'est pas la même que la "gamme" de Python 2, et est fonctionnellement la même que "xrange" de Python 2, j'ai aussi chronométré ce qui suit:

C:\**Python27_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in **xrange**(10000000): cnt += 1"
5 loops, best of 2: **320 msec** per loop

on peut facilement voir que la version 3.3 est presque deux fois plus lente que la version 2.7 et Python 3.4 est environ 10% plus lent que cela encore.

ma question: y a-t-il une option d'environnement ou un paramètre qui corrige cela, ou est-ce juste du code inefficace ou l'interpréteur qui en fait plus pour la version Python 3?


la réponse semble être que Python 3 utilise les entiers de "précision infinie" qui étaient appelés "long" en Python 2.x son type par défaut "int" sans aucune option pour utiliser la longueur fixe de bits de Python "int" et c'est le traitement de ces longueurs variables "int"'S qui prend le temps supplémentaire comme discuté dans les réponses et les commentaires ci-dessous.

il se peut que Python 3.4 soit un peu plus lent que Python 3.3 à cause des changements dans l'allocation de mémoire pour supporter la synchronisation qui ralentit légèrement l'allocation/désallocation de mémoire, ce qui est probablement la raison principale pour laquelle la version actuelle de" long " processing tourne plus lentement.

22
demandé sur GordonBGood 2014-05-04 09:57:42
la source

2 ответов

la différence est due au remplacement du type int par le type long . De toute évidence, les opérations avec de longs entiers vont être plus lentes parce que les opérations long sont plus complexes.

si vous forcez python2 à utiliser longs en paramétrant cnt à 0L la différence disparaît:

$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in range(10000000): cnt += 1L"
5 loops, best of 2: 1.1 sec per loop
$python3 -mtimeit -n5 -r2 -s"cnt=0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: 686 msec per loop
$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in xrange(10000000): cnt += 1L"
5 loops, best of 2: 714 msec per loop

comme vous pouvez le voir sur ma machine python3.4 est plus rapide que python2 en utilisant range et en utilisant xrange en utilisant long s. Le dernier benchmark avec 2 xrange de python montre que la différence dans ce cas est minime.

Je n'ai pas python3.3 installé, donc je ne peux pas faire une comparaison entre 3.3 et 3.4, mais pour autant que je sache rien de significatif changé entre ces deux versions (concernant range ), de sorte que les horaires devraient être à peu près les mêmes. Si vous voyez une différence significative essayer d'inspecter le bytecode généré en utilisant le dis module. Il y a eu un changement concernant les allocateurs de mémoire ( PEP 445 ) mais je n'ai aucune idée si les allocateurs de mémoire par défaut ont été modifiés et quelles conséquences il y avait performance-sage.

18
répondu Bakuriu 2016-06-30 21:58:15
la source

une réponse sommaire de ce que j'ai appris de cette question pourrait être utile à d'autres qui se demandent les mêmes choses que moi:

  1. la raison du ralentissement est que toutes les variables entières en Python 3.x sont maintenant " infinite precision "comme le type qui était appelé" long " en Python 2.x mais est maintenant le seul type entier comme décidé par PEP 237 . Selon ce document, "court" entiers si la profondeur de bits de l'architecture de base n'existait plus (ou seulement en interne).

  2. les anciennes opérations variables" courtes "pouvaient fonctionner assez rapidement parce qu'elles pouvaient utiliser directement les opérations de code machine sous-jacentes et optimiser l'allocation de nouveaux objets" int " parce qu'ils avaient toujours la même taille.

  3. le type" long " n'est actuellement représenté que par un objet de classe attribué en mémoire puisqu'il peut dépasser la profondeur de bits d'un registre de longueur/mémoire fixe donné; puisque ces représentations d'objets peuvent croître ou rétrécir pour diverses opérations et donc avoir une taille variable, on ne peut pas leur donner une allocation de mémoire fixe et les laisser là.

  4. ces types" longs "(actuellement) n'utilisent pas une taille de mot de l'architecture machine complète mais réservent un peu (normalement le bit signe) pour faire des contrôles de débordement, donc la" précision infinie long " est divisé (actuellement) en "chiffres" de tranche de 15 bits/30 bits pour les architectures 32 bits/64 bits, respectivement.

  5. bon nombre des utilisations communes de ces "longs" entiers ne nécessiteront pas plus d'un (ou peut-être deux pour les architectures 32 bits) "chiffres" comme la gamme d'un "chiffre" est d'environ un milliard/32768 pour les architectures 64 bits/32 bits, respectivement.

  6. le code " C " est raisonnablement efficace pour faire un ou deux chiffres" opérations, de sorte que le coût de la performance sur les entiers "courts" plus simples n'est pas tout que élevé pour de nombreux usages communs autant que le calcul réel va par rapport au temps nécessaire pour exécuter la boucle byte-code interprète.

  7. le plus grand succès de performance est probablement la constante allocations de mémoire / deallocations , une paire pour chaque boucle entière opérations qui est très coûteux, en particulier alors que Python se déplace pour prendre en charge le multi-threading avec les serrures de synchronisation (ce qui explique probablement pourquoi Python 3.4 est pire que 3.3).

  8. actuellement, la mise en œuvre assure toujours suffisamment de "chiffres" en attribuant un "chiffre" supplémentaire au-dessus de la taille réelle des "chiffres" utilisés pour le plus grand opérande s'il y a une possibilité qu'il pourrait "croître", faire l'opération (qui peut ou ne peut pas réellement utiliser ce "chiffre" supplémentaire), et normalise ensuite la longueur du résultat pour tenir compte du nombre réel de "chiffres" utilisés, qui peuvent en fait rester les mêmes (ou peut-être "rétrécir" pour certaines opérations); ceci est fait en réduisant simplement le nombre de taille dans la structure "longue" sans une nouvelle allocation, donc peut gaspiller un "chiffre" d'espace mémoire, mais économise le coût de performance d'un autre cycle d'allocation/désallocation.

  9. il y a de l'espoir pour une amélioration de la performance: pour plusieurs opérations il est possible de prédire si l'opération causera une "croissance" ou non - par exemple, pour une addition , il suffit de regarder les Bits les plus significatifs (MSB) et l'opération ne peut pas croître si les deux MSB sont zéro , ce qui sera le cas pour de nombreuses opérations de boucle/compteur; une soustraction ne "croîtra pas" selon les signes et MSB des deux opérandes; un quart de travail à gauche ne "croîtra" que si L'ESM en est un; et cetera.

  10. pour les cas où la déclaration est quelque chose comme "cnt += 1"/"i += step" et ainsi de suite (ouvrant la possibilité d'opérations en place pour de nombreux cas d'utilisation), une version "en place" des opérations pourrait être appelée qui ferait les vérifications rapides appropriées et n'attribuerait un nouvel objet que si une "croissance" était nécessaire, sinon faire l'opération à la place de la première opérande. La complication serait que le compilateur devrait produire ces bytes" en place " codes, cependant , cela a déjà été fait , avec les codes byte Spéciaux appropriés "en place" Produits, juste que l'interpréteur byte-code actuel les dirige vers la version habituelle comme décrit ci-dessus parce qu'ils n'ont pas encore été mis en œuvre (valeurs zéro'd/null dans le tableau).

  11. il se peut fort bien que tout ce qui doit être fait soit d'écrire des versions de ces "opérations en place" et de les remplir dans le tableau des méthodes "longues" avec le byte-code interprète déjà les trouver et les exécuter s'ils existent ou des changements mineurs à une table pour la faire appeler étant tout ce qui est nécessaire.

notez que les flotteurs sont toujours de la même taille, donc on pourrait faire les mêmes améliorations, bien que les flotteurs soient alloués en blocs d'emplacements de rechange pour une meilleure efficacité; il serait beaucoup plus difficile de le faire pour"long" 's comme ils prennent une quantité variable de mémoire.

Note complémentaire cela briserait l'immutabilité de "long"'s (et optionnellement float's), c'est pourquoi il n'y a pas d'opérateurs inplace définis, mais le fait qu'ils soient traités comme mutables seulement pour ces cas spéciaux n'affecte pas le monde extérieur car il ne se rendrait jamais compte que parfois un objet donné a la même adresse que l'ancienne valeur (aussi longtemps que les comparaisons d'égalité regardent les contenus et pas seulement les adresses d'objet).

je crois qu'en évitant la mémoire allocation/suppression pour ces cas d'utilisation courante, la performance de Python 3.x sera assez proche de Python 2.7.

une grande partie de ce que j'ai appris ici vient du fichier source Python trunk 'C' pour le "long" objet


EDIT_ADD: Whoops, oublié que si les variables sont parfois mutables, alors les fermetures sur les variables locales ne fonctionnent pas ou ne fonctionnent pas sans changements majeurs, ce qui signifie que les opérations en place ci-dessus "briseraient" les fermetures. Il semblerait qu'une meilleure solution serait d'obtenir une allocation de rechange anticipée qui fonctionne pour "long"'s tout comme il le faisait pour les entiers courts et le fait pour les flottants, même si seulement pour les cas où la taille "long" ne change pas (qui couvre la majorité du temps comme pour les boucles et les compteurs selon la question). Faire ceci devrait signifier que le code ne tourne pas beaucoup plus lentement que Python 2 pour un usage typique.

24
répondu GordonBGood 2014-05-05 06:04:19
la source