128 votes

Est "x < y < z" plus rapide que le "x < y et y < z"?

À partir de cette page, nous savons que:

Enchaînés les comparaisons sont plus rapide que d'utiliser l' andde l'opérateur. Écrire x < y < z au lieu de x < y and y < z.

Cependant, j'ai eu un résultat différent de tester 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 qu' x < y and y < z plus rapide que de l' x < y < z. Pourquoi?

Après des recherches sur certains messages sur ce site (comme cette une) je sais que "évaluées qu'une seule fois" est la clé pour x < y < z, cependant je suis toujours confus. Faire une étude plus approfondie, j'ai démonté ces deux fonctions à l'aide d' 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 la sortie 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 l' x < y and y < z a moins de dissembled de commandes que d' x < y < z. Devrais-je envisager x < y and y < z plus rapide que l' x < y < z?

Testé avec Python 2.7.6 sur un processeur Intel(R) Xeon(R) CPU E5640 @ 2.67 GHz.

110voto

Rob Points 251

La différence est que, en x < y < z y n'est évaluée qu'une fois. Cela ne fait pas une grande différence si y est une variable, mais il le fait quand il est un appel de fonction qui prend un certain temps à 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

22voto

Zack Points 44583

Optimale bytecode pour les deux fonctions que vous avez définie seraient

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

parce que le résultat de la comparaison n'est pas utilisé. Nous allons rendre la situation encore plus intéressant de retourner le résultat de la comparaison. Nous allons également avoir pour résultat de ne pas être connus 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

Là encore, les deux versions de la comparaison sont sémantiquement identiques, de sorte que le optimal bytecode est la même pour les deux constructions. Du mieux que je peux en sortir, il devrait ressembler à ceci. J'ai annoté chaque ligne avec le contenu de la pile avant et après chaque opcode, dans la Suite la notation (en haut de la pile de droite, -- divise avant et après, suiveur ? indique quelque chose qui pourrait ou ne pourrait pas être là). Notez que RETURN_VALUE ignore tout ce qui se passe à gauche sur la pile en dessous de la valeur retourné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 mise en œuvre de la langue, Disponible, PyPy, que ce soit, ne génère pas de ce bytecode (ou de son propre équivalent de la séquence des opérations) pour les deux variantes, qui démontre la mauvaise qualité de ce compilateur de bytecode. Le bytecode des séquences que vous avez posté ci-dessus est un problème résolu (je pense que tous vous avez besoin pour ce cas est de constantes, l'élimination du code mort, et une meilleure modélisation du contenu de la pile; commune de la sous-expression d'élimination serait également bon marché et de la valeur), et il n'y a vraiment aucune excuse pour ne pas le faire dans un langage moderne de mise en œuvre.

Maintenant, il arrive que toutes les implémentations actuelles de la langue ont la mauvaise qualité du bytecode compilateurs. Mais vous devez ignorer que, bien que le codage! Prétendre le compilateur bytecode est bonne, et l'écriture la plus lisible le code. Il sera probablement beaucoup assez vite de toute façon. Si ce n'est pas le cas, recherchez algorithmique des améliorations les premières, et de donner Cython un essai deuxième-qui fournit beaucoup plus d'amélioration pour le même effort que n'importe quel niveau de l'expression astuces que vous pouvez appliquer.

8voto

skyking Points 1919

Puisque la différence dans le résultat semble être dû à un manque d'optimisation, je pense que vous devriez ignorer cette différence pour la plupart des cas - c'est peut-être que la différence va s'en aller. La différence, c'est parce qu' y seulement devraient être évalués une fois, et qui est résolu par la dupliquer sur la pile, ce qui implique un extra - POP_TOP - la solution pour utiliser LOAD_FAST pourrait être possible.

La différence importante est que, en x<y and y<z deuxième y doit être évalué deux fois si x<y a la valeur true, ce qui a des implications si l'évaluation de l' y prend beaucoup de temps ou avoir des effets secondaires.

Dans la plupart des scénarios, vous devez utiliser x<y<z malgré le fait que c'est un peu plus lente.

6voto

Bakuriu Points 22607

Tout d'abord, votre comparaison est à peu près vide de sens parce que les deux constructions sont pas présentés pour fournir une amélioration de la performance, de sorte que vous ne devriez pas décider de les utiliser à la place de l'autre.

L' x < y < z construire:

  1. Est plus claire et plus directe dans son sens.
  2. Sa sémantique est ce que vous attendez de la mathématique "sens" de la comparaison: evalute x, y et z une fois et vérifier si l'ensemble de la condition est vraie. À l'aide de and la sémantique en évaluant y plusieurs fois, ce qui peut changer le résultat.

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

Ceci dit: plus code désassemblé ne n' implique plus lente code. Cependant l'exécution de plus de bytecode des opérations, c'est que chaque opération est plus simple et pourtant, il nécessite une itération de la boucle principale. Cela signifie que si les opérations que vous effectuez sont extrêmement rapides (par exemple, la variable locale de recherche que vous êtes en train de faire là), puis les frais généraux de l'exécution de plus de bytecode, d'opérations de la matière.

Mais notez que ce résultat ne signifie pas tenir dans le plus générique de la situation, seulement pour le "pire des cas" où il vous arriverait de profil. Comme d'autres l'ont noté, si vous modifiez y quelque chose qui prend encore un peu plus de temps, vous verrez que les résultats de la modifier, parce que l'enchaînement de notation évalue qu'une seule fois.

En résumant:

  • Considérer la sémantique avant la performance.
  • Prendre en compte la lisibilité.
  • Ne faites pas confiance à micro-benchmarks. Toujours de profil, avec différents types de paramètres de voir comment une fonction et l'expression de calendrier se comporter par rapport à ladite paramètres et envisager la façon dont vous prévoyez de l'utiliser.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X