なぜ早期復帰が他よりも遅いのですか?


179

これは、私が数日前に答えたフォローアップの質問です。編集:その質問のOPは、私が彼に投稿したコードを使用して同じ質問をしているようですが、私はそれを知りませんでした。謝罪。提供される答えは異なります!

実質的に私はそれを観察しました:

>>> def without_else(param=False):
...     if param:
...         return 1
...     return 0
>>> def with_else(param=False):
...     if param:
...         return 1
...     else:
...         return 0
>>> from timeit import Timer as T
>>> T(lambda : without_else()).repeat()
[0.3011460304260254, 0.2866089344024658, 0.2871549129486084]
>>> T(lambda : with_else()).repeat()
[0.27536892890930176, 0.2693932056427002, 0.27011704444885254]
>>> T(lambda : without_else(True)).repeat()
[0.3383951187133789, 0.32756996154785156, 0.3279120922088623]
>>> T(lambda : with_else(True)).repeat()
[0.3305950164794922, 0.32186388969421387, 0.3209099769592285]

...または言い換えるelseと、if条件がトリガーされているかどうかに関係なく、句がある方が高速です。

私はそれが2つによって生成された異なるバイトコードに関係していると思いますが、誰でも詳細に確認/説明できますか?

編集:誰もが私のタイミングを再現できるわけではないようですので、私のシステムにいくつかの情報を提供することが役立つかもしれないと思いました。デフォルトのpythonがインストールされたUbuntu 11.10 64ビットを実行しています。python次のバージョン情報を生成します。

Python 2.7.2+ (default, Oct  4 2011, 20:06:09) 
[GCC 4.6.1] on linux2

Python 2.7の逆アセンブリの結果は次のとおりです。

>>> dis.dis(without_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  4     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
>>> dis.dis(with_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  5     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        

1
SOについて同じ質問がありましたが、今は見つかりません。彼らは生成されたバイトコードをチェックしました、そしてもう一つのステップがありました。観察された差異はテスター(マシン、SO ..)に大きく依存しており、ごくわずかな差異しか見られなかった。
joaquin

3
3.xでは、両方とも、の最後に到達不能コード(同じように到達することはありません)に対して同じバイトコードを保存します。私はデッドコードが機能をより速くすることを強く疑っています。誰かが2.7を提供できますか?LOAD_CONST(None); RETURN_VALUEwith_elsedis

4
これは再現できませんでした。機能がelseありFalse、それらの中で最も遅い(152ns)。2番目に速いのはTrueなしelse(143ns)で、他の2つは基本的に同じでした(137nsと138ns)。デフォルトのパラメーターを使用せず%timeit、iPythonで測定しました。
rplnt

私は時々 、これは...彼らは私のためにかなり似ているように見える、without_elseバージョンで、時々 with_elseが速くなり、それらのタイミングを再現することはできません
セドリック・ジュリアン

1
分解結果を追加しました。私はUbuntu 11.10、64ビット、ストックPython 2.7を使用しています-@macと同じ構成。また、それwith_elseは明らかに高速です。
Chris Morgan、

回答:


387

これは純粋な推測であり、正しいかどうかを確認する簡単な方法はわかりませんが、私には理論があります。

私はあなたのコードを試してみましたが、結果の同じを取得し、without_else()繰り返しわずかに遅いよりwith_else()

>>> T(lambda : without_else()).repeat()
[0.42015745017874906, 0.3188967452567226, 0.31984281521812363]
>>> T(lambda : with_else()).repeat()
[0.36009842032996175, 0.28962249392031936, 0.2927151355828528]
>>> T(lambda : without_else(True)).repeat()
[0.31709728471076915, 0.3172671387005721, 0.3285821242644147]
>>> T(lambda : with_else(True)).repeat()
[0.30939889008243426, 0.3035132258429485, 0.3046679117038593]

バイトコードが同一であることを考えると、唯一の違いは関数の名前です。特に、タイミングテストはグローバル名を検索します。名前を変更するwithout_else()と、違いが消えます。

>>> def no_else(param=False):
    if param:
        return 1
    return 0

>>> T(lambda : no_else()).repeat()
[0.3359846013948413, 0.29025818923918223, 0.2921801513879245]
>>> T(lambda : no_else(True)).repeat()
[0.3810395594970828, 0.2969634408842694, 0.2960104566362247]

私の推測では、without_else他の何かとハッシュの衝突があるglobals()ため、グローバル名の検索が少し遅くなります。

編集:7つまたは8つのキーを持つ辞書はおそらく32のスロットを持っているので、それに基づいwithout_elseてハッシュ衝突があり__builtins__ます:

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('with_else', 9), ('__package__', 15), ('without_else', 8), ('T', 21), ('__name__', 25), ('no_else', 28), ('__doc__', 29)]

ハッシングの仕組みを明確にするには:

__builtins__ ハッシュが-1196389688になり、テーブルサイズ(32)を法として削減されたことは、テーブルの#8スロットに格納されることを意味します。

without_else32を法とする505688136へのハッシュは8なので、衝突が発生します。このPythonを解決するには、以下を計算します。

で始まります:

j = hash % 32
perturb = hash

空きスロットが見つかるまでこれを繰り返します。

j = (5*j) + 1 + perturb;
perturb >>= 5;
use j % 2**i as the next table index;

これにより、次のインデックスとして使用する17が与えられます。幸い、それは無料ですので、ループは1回だけ繰り返されます。ハッシュテーブルのサイズは2の累乗であるので、2**iハッシュ・テーブルのサイズであり、iハッシュ値から使用されるビットの数ですj

テーブルへの各プローブはこれらの1つを見つけることができます:

  • スロットは空です。その場合、プローブが停止し、値がテーブルにないことがわかります。

  • スロットは未使用ですが、過去に使用された場合は、上記のように計算された次の値を試します。

  • スロットはいっぱいですが、テーブルに保存されている完全なハッシュ値は、探しているキーのハッシュと同じではありません(それが__builtins__vs の場合に起こりwithout_elseます)。

  • スロットがいっぱいで、必要なハッシュ値が正確にある場合、Pythonは、キーと検索しているオブジェクトが同じオブジェクトであるかどうかを確認します(この場合は、識別子である可能性のある短い文字列がインターンされているためです)同一の識別子はまったく同じ文字列を使用します)。

  • 最後に、スロットがいっぱいになると、ハッシュは正確に一致しますが、キーは同じオブジェクトではないため、Pythonがそれらを比較して等しいかどうかを確認します。これは比較的低速ですが、名前の検索の場合は実際には発生しません。


9
@Chris、文字列の長さは重要ではありません。初めて文字列をハッシュする場合、文字列の長さに比例して時間がかかりますが、計算されたハッシュは文字列オブジェクトにキャッシュされるため、後続のハッシュはO(1)になります。
ダンカン

1
ええと、私はキャッシュに気づいていませんでしたが、それは理にかなっています
Chris Eberle

9
魅力的です!シャーロックと呼んでもいいですか?;)とにかく、質問が適格になり次第、賞金でいくつかの追加ポイントを与えることを忘れないことを願っています。
Voo

4
@mac、まったく違います。ハッシュの解決について少し追加します(コメントに詰め込みますが、思ったより面白いです)。
ダンカン

6
@Duncan-ハッシュプロセスの説明に時間を割いていただき、ありがとうございます。一流の答え!:)
mac
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.