「x <y <z」は「x <y and y <z」よりも高速ですか?


129

このページから、次のことがわかります

連鎖比較は、and演算子を使用するよりも高速です。のx < y < z代わりに書いてくださいx < y and y < z

ただし、次のコードスニペットをテストすると異なる結果が得られました。

$ 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

それx < y and y < zよりも速いようですx < y < zどうして?

(のようなこのサイトでは、いくつかの記事を検索した後、この1)私は、「一度だけ評価」するためのキーである知っているx < y < zが、私はまだ混乱しています、。さらに調査するために、私はこれらの2つの関数を次のように分解しました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)

そして出力は:

## 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)

は、x < y and y < zよりも分解されていないコマンドのようですx < y < z。私はx < y and y < zもっと速く考えるべきx < y < zですか?

Intel(R)Xeon(R)CPU E5640 @ 2.67GHzでPython 2.7.6を使用してテスト済み。


8
解読されたコマンドが増えても、コードが複雑になったり遅くなったりするわけではありません。しかし、あなたのtimeitテストを見て私はこれに興味を持ちました。
Marco Bonelli、2015

6
「一度評価した」との速度の違いyは、変数のルックアップだけでなく、関数呼び出しのようなよりコストのかかるプロセスが原因であると想定しました。つまり、両方の比較で1回呼び出されるため10 < max(range(100)) < 15よりも高速です。10 < max(range(100)) and max(range(100)) < 15max(range(100))
zehnpaard

2
@MarcoBonelliそれがないときに逆アセンブルコード1)その時点でメインループのオーバーヘッドが顕著になるので、すべての単一のバイトコードは、非常に非常に高速である)ループを含み、2ありません。
バクリウ

2
分岐予測はテストを台無しにするかもしれません。
Corey Ogburn、2015

2
@zehnpaard私はあなたに同意します。"y"が単純な値(関数呼び出しや計算など)を超える場合、 "<"がx <y <zで1回評価されると、はるかに顕著な影響があると思います。提示されたケースでは、エラーバーの範囲内にあります。(失敗した)分岐予測の影響、最適化されていないバイトコード、およびその他のプラットフォーム/ CPU固有の影響が支配的です。これは、一度評価する方が読みやすくなるという一般的な規則を無効にするものではありませんが、極端な場合にはそれほど多くの価値が追加されない可能性があることを示しています。
MartyMacGyver

回答:


111

違いは、in x < y < z yは1回しか評価されないことです。yが変数の場合は大きな違いはありませんが、関数呼び出しの場合は計算に時間がかかります。

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

18
もちろん、意味の違いもあるかもしれません。y()は2つの異なる値を返すだけでなく、変数を使用すると、x <yの小なり演算子の評価によってyの値が変わる可能性があります。これがバイトコードで最適化されないことがよくある理由です(yが非ローカル変数またはクロージャに参加している変数などの場合)
Random832

気になるのは、なぜsleep()関数の内部が必要なのですか?
教授

@Prof結果の計算に時間がかかる関数をシミュレートします。関数がすぐに戻る場合、2つのtimeitの結果に大きな違いはありません。
Rob

@Robなぜそれほど大きな違いがないのですか?それは3ms vs 205msでしょう、それはそれが十分にそれを実証するのではないでしょうか?
教授

@Prof y()が2回計算される点が欠けているため、1x200msではなく2x200msになります。残り(3/5 ms)は、タイミング測定における無関係なノイズです。
Rob

22

定義した両方の関数の最適なバイトコードは

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

比較の結果は使用されないためです。比較の結果を返すことで、状況をより興味深いものにしましょう。また、コンパイル時に結果がわからないようにしましょう。

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

この場合も、2つの比較バージョンは意味的に同一であるため、最適なバイトコードは両方の構成で同じです。できる限りうまくやると、こんな感じになります。Forth表記で各オペコードの前後にスタックの内容で各行に注釈を付けました(右側のスタックの最上部、--前後で分割、末尾?はそこにある場合とない場合があることを示しています)。RETURN_VALUE返された値の下のスタックに残っているすべてを破棄することに注意してください。

          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  --

CPython、PyPyなどの言語の実装が両方のバリエーションに対してこのバイトコード(またはそれと同等の一連の操作)を生成しない場合、そのバイトコードコンパイラーの品質が低いことを示しています。上記に投稿したバイトコードシーケンスから取得することで問題は解決しました(この場合に必要なのは、定数の折りたたみデッドコードの除去、スタックの内容のより良いモデリングだけです。一般的な部分式の除去も安くて価値があります。)、そして現代の言語実装でそれを行わないための言い訳は本当にありません。

現在、この言語の現在のすべての実装には、低品質のバイトコードコンパイラーがあります。ただし、コーディング中は無視してください。バイトコードコンパイラが良いふりをして、最も読みやすいコードを書きます。とにかく、それはおそらく十分に速いでしょう。そうでない場合は、最初にアルゴリズムの改善を探し、次にCythonを試してみてください。これは、適用する可能性のある式レベルの微調整よりもはるかに多くの改善を同じ作業に提供します。


さて、すべての最適化の中で最も重要なのは、そもそもインライン化が可能なはずです。そして、それは関数の実装を動的に変更できる動的言語の「解決された問題」とはかけ離れています(ただし、実行可能です-HotSpotは同様のことを行うことができ、GralがPythonや他の動的言語でこれらの種類の最適化を利用できるように取り組んでいます) )。そして、関数自体が異なるモジュールから呼び出される可能性があるため(または呼び出しが動的に生成される可能性があります!)、これらの最適化を実際に行うことはできません。
Voo

1
@Voo私の手で最適化されたバイトコードは、任意のダイナミズムが存在する場合でも、オリジナルとまったく同じセマンティクスを持っています(1つの例外:a <b≡b> aが想定されます)。また、インライン化は過大評価されています。あなたがそれをやりすぎると-そしてそれをやりすぎるのは簡単すぎる-I-キャッシュを破壊し、あなたが得たすべてを失い、そしていくつかを失う。
zwol 2015

あなたは正しいです私はあなたinteresting_compareが上部の単純なバイトコード(インライン化でのみ機能する)に最適化したかったと思ったと思いました。完全にトピック外ですが、インライン化はコンパイラにとって最も重要な最適化の1つです。実際のプログラムで言うHotSpotを使用していくつかのベンチマークを実行することができます(手動で最適化された1つのホットループで99%の時間を費やすいくつかの数学のテストではありません)。何かをインライン化する機能-大きなリグレッションが見られます。
VOO

@Vooええ、上部の単純なバイトコードは、OPの元のコードの「最適なバージョン」であるはずでしたinteresting_compare
zwol

「1つの例外:a <b≡b> aが想定されます」→これは、Pythonでは単に当てはまりません。さらに、CPythonにyはデバッグツールがたくさんあるので、CPythonでの操作がスタックを変更しないことを本当に想定することもできないと思います。
Veedrac

8

出力の違いは最適化の欠如が原因であると思われるため、ほとんどの場合、その違いを無視する必要があると思います。違いがなくなる可能性があります。違いは、y一度だけ評価されるべきであり、それは追加を必要とするスタックでそれを複製することによって解決されるためですPOP_TOP-使用するソリューションLOAD_FAST可能かもしれません。

ただし、重要な違いはx<y and y<z、2番目のyx<ytrue と評価された場合に2回評価する必要があるということです。これは、yかなりの時間かかるや副作用がある場合に影響します。

ほとんどのシナリオではx<y<z、多少遅いという事実にもかかわらず使用する必要があります。


6

まず、パフォーマンスを向上させるために2つの異なる構成が導入されていないため、比較はほとんど意味がありません

x < y < z構文:

  1. その意味においてより明確でより直接的です。
  2. そのセマンティクスは、比較の「数学的意味」から期待するものです。evalute xyそしてz onceと、条件全体が満たされているかどうかをチェックします。を使用andすると、y複数回評価することでセマンティクスが変更され、結果変わる可能性があります。

だから、あなたがして欲しいのセマンティクスに応じて、他の場所でいずれかを選択した場合それらが同等であるは、一方が他方より読みやすいかどうかを判断します。

これは言った:より逆アセンブルされたコードはしません遅くなる。ただし、より多くのバイトコード操作を実行すると、各操作がより簡単になりますが、メインループの反復が必要になります。この手段ならば(あなたがやっているよう例えばローカル変数を参照)、実行している操作は非常に高速です、そしてオーバーヘッドより多くのバイトコード操作を実行するのは問題ことができます。

ただし、この結果は、より一般的な状況で保持されず、プロファイリングする「最悪の場合」にのみ適用されることに注意してください。他の人が指摘しているように、y少し時間がかかるものに変更すると、連鎖表記は一度しか評価しないため、結果が変わることがわかります。

要約:

  • パフォーマンスの前にセマンティクスを検討してください。
  • 読みやすさを考慮してください。
  • マイクロベンチマークを信用しないでください。常に異なる種類のパラメーターでプロファイルを作成して、関数/式のタイミングが上記のパラメーターとの関係でどのように動作するかを確認し、その使用方法を検討してください。

5
あなたの答えには、引用されたページ、特に質問の場合-フロートの比較が単に間違っているという、直接的で関連性のある事実は含まれていないと思います。連鎖比較は、測定値と生成されたバイトコードの両方で見られるように高速ではありません。
pvg

30
パフォーマンスのタグが付いた質問に「パフォーマンスについてあまり考えるべきではない」と答えることは、私には役に立たないようです。質問者が一般的なプログラミングの原則を理解していることについて、潜在的にひいきにすることを想定しており、次に、目前の問題ではなく、それらについて主に話し合っています。
Ben Millwood

@Veerdacあなたはコメントを誤解しています。OPが依存していた元のドキュメントで提案された最適化は、最低でもフロートの場合は間違っています。速くはありません。
pvg
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.