web-dev-qa-db-ja.com

早期返還が他より遅いのはなぜですか?

これは 数日前に回答した へのフォローアップの質問です。 編集:その質問の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つによって生成された異なるバイトコードに関係していると思いますが、誰かが詳細に確認/説明することはできますか?

EDIT:すべての人が私のタイミングを再現できるわけではないようですので、システムに関する情報を提供することが役立つと思いました。デフォルトのpythonインストール済み。Ubuntu11.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        
170
mac

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

私はあなたのコードを試して、同じ結果を得ました。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_else_は505688136にハッシュし、32を法として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から使用されるビット数です。

テーブルへの各プローブは、次のいずれかを見つけることができます。

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

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

  • スロットはいっぱいですが、テーブルに保存されている完全なハッシュ値は、探しているキーのハッシュとは異なります(これは___builtins___ vs _without_else_の場合に起こります)。

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

  • 最後に、スロットがいっぱいになると、ハッシュは完全に一致しますが、キーは同一のオブジェクトではないため、Python同等かどうかを比較してみてください。これは比較的遅いですが、名前検索のケースは実際には起こらないはずです。

373
Duncan