web-dev-qa-db-fra.com

"X <y <z" est-il plus rapide que "x <y et y <z"?

De cette page , nous savons que:

Les comparaisons chaînées sont plus rapides qu'avec l'opérateur and. Écrire x < y < z au lieu de x < 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 paraît que x < y and y < z est plus rapide que x < y < z. Pourquoi?

Après avoir cherché quelques articles sur ce site (comme celui-ci ), je sais que "évalué une seule fois" est la clé pour x < y < z, cependant je suis toujours confus. Pour approfondir l’étude, 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 dissemblables que x < y < z. Devrais-je considérer 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.

129
zangw

La différence est que dans x < y < zy n’est évalué qu’une fois. Cela ne fait pas une grande différence si y est une variable, mais le fait quand il s'agit d'un appel de fonction, ce qui prend un certain temps pour calculer.

from time import sleep
def y():
    sleep(.2)
    return 1.3
%timeit 1.2 < y() < 1.8
10 loops, best of 3: 203 ms per loop
%timeit 1.2 < y() and y() < 1.8
1 loops, best of 3: 405 ms per loop
110
Rob

Optimal bytecode pour les deux fonctions que vous avez définies serait

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

parce que 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 en sorte que le résultat ne soit pas connu au moment de la compilation.

def interesting_compare(y):
    x = 1.1
    z = 1.3
    return x < y < z  # or: x < y and y < z

De nouveau, les deux versions de la comparaison sont sémantiquement identiques, donc le bytecode optimal est le même pour les deux constructions. Au mieux, je pourrais y arriver, ça ressemblerait à ceci. J'ai annoté chaque ligne avec le contenu de la pile avant et après chaque opcode, en notation Forth (sommet de la pile à droite, -- divise avant et après, à la fin ? indique quelque chose qui pourrait être ou ne pas être là). Notez que RETURN_VALUE supprime tout ce qui se trouve sur la pile sous la valeur renvoyée.

          0 LOAD_FAST                0 (y)    ;          -- y
          3 DUP_TOP                           ; y        -- y y
          4 LOAD_CONST               0 (1.1)  ; y y      -- y y 1.1
          7 COMPARE_OP               4 (>)    ; y y 1.1  -- y pred
         10 JUMP_IF_FALSE_OR_POP     19       ; y pred   -- y
         13 LOAD_CONST               1 (1.3)  ; y        -- y 1.3
         16 COMPARE_OP               0 (<)    ; y 1.3    -- pred
     >>  19 RETURN_VALUE                      ; y? pred  --

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 bytecodes. Obtenir des séquences de bytecode que vous avez postées ci-dessus est un problème résolu (je pense que tout ce dont vous avez besoin dans ce cas est pliage constant , élimination du code mort , et une meilleure modélisation de la contenu de la pile; élimination de sous-expression commune serait également bon marché et précieux), et il n’ya vraiment aucune excuse pour ne pas le faire dans une implémentation en langage moderne.

Or, il arrive que toutes les implémentations actuelles du langage utilisent des compilateurs de code-octet de mauvaise qualité. Mais vous devriez ignorer pendant le codage! Prétendez que le compilateur de code-octets est bon et écrivez le code le plus lisible. De toute façon, ce sera probablement assez rapide. Si ce n'est pas le cas, cherchez d'abord les améliorations algorithmiques, puis donnez Cython une seconde à l'essai - cela apportera beaucoup plus d'amélioration pour le même effort que tous les réglages au niveau de l'expression que vous pourriez appliquer.

22
zwol

Étant donné que la différence dans la sortie semble être due à un manque d'optimisation, je pense que vous devriez ignorer cette différence dans la plupart des cas - il est possible que la différence disparaisse. La différence est que y ne devrait être évalué qu’une seule fois et qu’il est résolu en le dupliquant sur la pile, ce qui nécessite un supplément de POP_TOP - la solution à utiliser LOAD_FAST pourrait être possible cependant.

La différence importante cependant est que dans x<y and y<z le second y devrait être évalué deux fois si x<y est évalué à true, cela a des implications si l'évaluation de y prend beaucoup de temps ou a des effets secondaires.

Dans la plupart des scénarios, vous devez utiliser x<y<z malgré le fait que ce soit un peu plus lent.

8
skyking

Tout d’abord, votre comparaison n’a pas de sens, car les deux concepts différents ont été not introduits dans le but d’améliorer les performances. Vous ne devez donc pas décider d’utiliser celui-ci à la place de celui-ci.

Le x < y < z construction:

  1. Est plus clair et plus direct dans son sens.
  2. Sa sémantique correspond à ce que vous attendez du "sens mathématique" de la comparaison: evalute x, y et z une fois et vérifiez si la condition entière est vérifiée. Utiliser and change la sémantique en évaluant y plusieurs fois, ce qui peut changer le résultat .

Donc choisissez l'une à la place de l'autre en fonction de la sémantique que vous voulez et, si elles sont équivalentes, si l'une est plus lisible que l'autre.

Ceci dit: plus de code désassemblé ne ne signifie pas un code plus lent. Cependant, si vous exécutez davantage d’opérations bytecode, 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 variable locale comme vous le faites là-bas), puis la surcharge d’exécuter plus d’opérations de bytecode peut avoir une importance.

Mais notez que ce résultat not reste dans la situation la plus générique, uniquement dans le "pire des cas" que vous rencontrez pour profiler. Comme d'autres l'ont fait remarquer, si vous modifiez y par 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 fois.

Résumant:

  • Considérer la sémantique avant la performance.
  • Prendre en compte la lisibilité.
  • Ne faites pas confiance aux micro-critères. Profilez toujours avec différents types de paramètres pour voir comment se comporte un minutage fonction/expression par rapport à ces paramètres et comment vous prévoyez de l’utiliser.
6
Bakuriu