De cette page , nous savons que:
Les comparaisons chaînées sont plus rapides que l'utilisation de l'
and
opérateur. Écrivezx < y < z
au lieu dex < y and y < z
.
Cependant, j'ai obtenu un résultat différent en testant les extraits de code suivants:
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y < z"
1000000 loops, best of 3: 0.322 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y and y < z"
1000000 loops, best of 3: 0.22 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y < z"
1000000 loops, best of 3: 0.279 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y and y < z"
1000000 loops, best of 3: 0.215 usec per loop
Il semble que ce x < y and y < z
soit plus rapide que x < y < z
. Pourquoi?
Après avoir recherché quelques articles sur ce site (comme celui-ci ), je sais que "évalué une seule fois" est la clé x < y < z
, mais je suis toujours confus. Pour approfondir mes études, j'ai démonté ces deux fonctions en utilisant dis.dis
:
import dis
def chained_compare():
x = 1.2
y = 1.3
z = 1.1
x < y < z
def and_compare():
x = 1.2
y = 1.3
z = 1.1
x < y and y < z
dis.dis(chained_compare)
dis.dis(and_compare)
Et le résultat est:
## chained_compare ##
4 0 LOAD_CONST 1 (1.2)
3 STORE_FAST 0 (x)
5 6 LOAD_CONST 2 (1.3)
9 STORE_FAST 1 (y)
6 12 LOAD_CONST 3 (1.1)
15 STORE_FAST 2 (z)
7 18 LOAD_FAST 0 (x)
21 LOAD_FAST 1 (y)
24 DUP_TOP
25 ROT_THREE
26 COMPARE_OP 0 (<)
29 JUMP_IF_FALSE_OR_POP 41
32 LOAD_FAST 2 (z)
35 COMPARE_OP 0 (<)
38 JUMP_FORWARD 2 (to 43)
>> 41 ROT_TWO
42 POP_TOP
>> 43 POP_TOP
44 LOAD_CONST 0 (None)
47 RETURN_VALUE
## and_compare ##
10 0 LOAD_CONST 1 (1.2)
3 STORE_FAST 0 (x)
11 6 LOAD_CONST 2 (1.3)
9 STORE_FAST 1 (y)
12 12 LOAD_CONST 3 (1.1)
15 STORE_FAST 2 (z)
13 18 LOAD_FAST 0 (x)
21 LOAD_FAST 1 (y)
24 COMPARE_OP 0 (<)
27 JUMP_IF_FALSE_OR_POP 39
30 LOAD_FAST 1 (y)
33 LOAD_FAST 2 (z)
36 COMPARE_OP 0 (<)
>> 39 POP_TOP
40 LOAD_CONST 0 (None)
Il semble que le x < y and y < z
a moins de commandes dissimulées que x < y < z
. Dois-je envisager x < y and y < z
plus rapide que x < y < z
?
Testé avec Python 2.7.6 sur un processeur Intel (R) Xeon (R) E5640 à 2,67 GHz.
la source
timeit
tests, cela m'a intéressé.y
n'y avait pas qu'une simple recherche de variable, mais un processus plus coûteux comme un appel de fonction? Ie10 < max(range(100)) < 15
est plus rapide que10 < max(range(100)) and max(range(100)) < 15
carmax(range(100))
est appelé une fois pour les deux comparaisons.Réponses:
La différence est que in
x < y < z
y
n'est évalué qu'une seule fois. Cela ne fait pas une grande différence si y est une variable, mais c'est le cas lorsqu'il s'agit d'un appel de fonction, ce qui prend un certain temps à calculer.la source
sleep()
intérieur de la fonction?Le bytecode optimal pour les deux fonctions que vous avez définies serait
car le résultat de la comparaison n'est pas utilisé. Rendons la situation plus intéressante en renvoyant le résultat de la comparaison. Faisons également en sorte que le résultat ne soit pas connaissable au moment de la compilation.
Encore une fois, les deux versions de la comparaison sont sémantiquement identiques, de sorte que le bytecode optimal est le même pour les deux constructions. Du mieux que je peux y arriver, cela ressemblerait à ceci. J'ai annoté chaque ligne avec le contenu de la pile avant et après chaque opcode, en notation Forth (haut de la pile à droite, se
--
divise avant et après, la fin?
indique quelque chose qui pourrait ou non être là). Notez queRETURN_VALUE
supprime tout ce qui reste sur la pile sous la valeur renvoyée.Si une implémentation du langage, CPython, PyPy, peu importe, ne génère pas ce bytecode (ou sa propre séquence équivalente d'opérations) pour les deux variantes, cela démontre la mauvaise qualité de ce compilateur de bytecode . Obtenir des séquences de bytecode que vous avez publiées ci-dessus est un problème résolu (je pense que tout ce dont vous avez besoin dans ce cas est un pliage constant , une élimination du code mort et une meilleure modélisation du contenu de la pile; l'élimination des sous-expressions courantes serait également bon marché et précieuse ), et il n'y a vraiment aucune excuse pour ne pas le faire dans une implémentation en langage moderne.
Maintenant, il arrive que toutes les implémentations actuelles du langage aient des compilateurs bytecode de mauvaise qualité. Mais vous devez ignorer cela lors du codage! Prétendez que le compilateur de bytecode est bon et écrivez le code le plus lisible . Ce sera probablement assez rapide de toute façon. Si ce n'est pas le cas, recherchez d'abord les améliorations algorithmiques et essayez ensuite Cython - cela fournira beaucoup plus d'amélioration pour le même effort que tout ajustement au niveau de l'expression que vous pourriez appliquer.
la source
interesting_compare
bytecode simple en haut (qui ne fonctionnerait qu'avec l'inlining). Complètement hors-sujet mais: Inlining est l'une des optimisations les plus essentielles pour tout compilateur. Vous pouvez essayer d'exécuter des tests avec, disons HotSpot, sur de vrais programmes (pas des tests mathématiques qui passent 99% de leur temps dans une boucle chaude optimisée à la main [et donc n'a plus d'appels de fonction de toute façon]) lorsque vous désactivez son capacité à tout insérer - vous verrez de grandes régressions.interesting_compare
.y
ne changent pas la pile, car il dispose de nombreux outils de débogage.Étant donné que la différence de sortie semble être due à un manque d'optimisation, je pense que vous devriez ignorer cette différence dans la plupart des cas - il se peut que la différence disparaisse. La différence est que
y
ne devrait être évalué qu'une seule fois et que cela est résolu en le dupliquant sur la pile, ce qui nécessite un supplémentPOP_TOP
- la solution à utiliserLOAD_FAST
pourrait cependant être possible.La différence importante est cependant que dans
x<y and y<z
la secondey
devrait être évaluée deux fois si elle estx<y
évaluée à vrai, cela a des implications si l'évaluation dey
prend beaucoup de temps ou a des effets secondaires.Dans la plupart des scénarios, vous devez utiliser
x<y<z
malgré le fait qu'il soit un peu plus lent.la source
Tout d'abord, votre comparaison n'a pratiquement aucun sens, car les deux constructions différentes n'ont pas été introduites pour améliorer les performances, vous ne devriez donc pas décider d'utiliser l'une à la place de l'autre en fonction de cela.
La
x < y < z
construction:x
,y
etz
une fois et vérifier si la condition entière est vérifiée . L'utilisationand
change la sémantique en évaluanty
plusieurs fois, ce qui peut changer le résultat .Alors choisissez-en un à la place de l'autre en fonction de la sémantique que vous souhaitez et, si sont équivalents, si l'un est plus lisible que l'autre.
Cela dit: plus de code désassemblé n'implique pas un code plus lent. Cependant, exécuter plus d'opérations de bytecode signifie que chaque opération est plus simple et nécessite cependant une itération de la boucle principale. Cela signifie que si les opérations que vous effectuez sont extrêmement rapides (par exemple, la recherche de variables locales comme vous le faites là-bas), alors la surcharge liée à l'exécution de plusieurs opérations de bytecode peut avoir de l'importance.
Mais notez que ce résultat ne vaut pas dans la situation plus générique, mais uniquement dans le «pire des cas» que vous profilez. Comme d'autres l'ont noté, si vous changez
y
à quelque chose qui prend encore un peu plus de temps, vous verrez que les résultats changent, car la notation chaînée ne l'évalue qu'une seule fois.En résumé:
la source