なぜ 'x'は( 'x'、)で 'x' == 'x'より速いのですか?


274
>>> timeit.timeit("'x' in ('x',)")
0.04869917374131205
>>> timeit.timeit("'x' == 'x'")
0.06144205736110564

また、複数の要素を持つタプルにも機能します。どちらのバージョンも直線的に増加するようです。

>>> timeit.timeit("'x' in ('x', 'y')")
0.04866674801541748
>>> timeit.timeit("'x' == 'x' or 'x' == 'y'")
0.06565782838087131
>>> timeit.timeit("'x' in ('y', 'x')")
0.08975995576448526
>>> timeit.timeit("'x' == 'y' or 'x' == 'y'")
0.12992391047427532

これに基づいて、私はどこでも完全に使い始めるべきだと思います!in==


167
念のため:のin代わりにどこでも使用しないでください==。読みやすさを損なうのは時期尚早の最適化です。
大佐32 2

4
試す x ="!foo" x in ("!foo",)x == "!foo"
パドレイクカニンガムに

2
BのA =値、C == D値と型の比較
dsgdfg

6
使用するより合理的なアプローチinの代わりには、==Cに切り替えることである
マッド物理学者

1
Pythonで作成していて、速度を上げるためにある構造を別の構造よりも選択する場合、それは間違っています。
Veky 2017

回答:


257

David Woleverに述べたように、これには目に見える以上のものがあります。どちらのメソッドもにディスパッチしisます。あなたはこれを証明することができます

min(Timer("x == x", setup="x = 'a' * 1000000").repeat(10, 10000))
#>>> 0.00045456900261342525

min(Timer("x == y", setup="x = 'a' * 1000000; y = 'a' * 1000000").repeat(10, 10000))
#>>> 0.5256857610074803

1つ目は、IDでチェックするため、非常に高速になる可能性があります。

一方が他方よりも長くかかる理由を見つけるために、実行をトレースしてみましょう。

彼らは両方で開始ceval.cから、COMPARE_OPそれが関係するバイトコードがあるので、

TARGET(COMPARE_OP) {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *res = cmp_outcome(oparg, left, right);
    Py_DECREF(left);
    Py_DECREF(right);
    SET_TOP(res);
    if (res == NULL)
        goto error;
    PREDICT(POP_JUMP_IF_FALSE);
    PREDICT(POP_JUMP_IF_TRUE);
    DISPATCH();
}

これはスタックから値をポップします(技術的には1つしかポップしません)

PyObject *right = POP();
PyObject *left = TOP();

そして比較を実行します:

PyObject *res = cmp_outcome(oparg, left, right);

cmp_outcome これは:

static PyObject *
cmp_outcome(int op, PyObject *v, PyObject *w)
{
    int res = 0;
    switch (op) {
    case PyCmp_IS: ...
    case PyCmp_IS_NOT: ...
    case PyCmp_IN:
        res = PySequence_Contains(w, v);
        if (res < 0)
            return NULL;
        break;
    case PyCmp_NOT_IN: ...
    case PyCmp_EXC_MATCH: ...
    default:
        return PyObject_RichCompare(v, w, op);
    }
    v = res ? Py_True : Py_False;
    Py_INCREF(v);
    return v;
}

ここでパスが分割されます。PyCmp_INブランチはありません

int
PySequence_Contains(PyObject *seq, PyObject *ob)
{
    Py_ssize_t result;
    PySequenceMethods *sqm = seq->ob_type->tp_as_sequence;
    if (sqm != NULL && sqm->sq_contains != NULL)
        return (*sqm->sq_contains)(seq, ob);
    result = _PySequence_IterSearch(seq, ob, PY_ITERSEARCH_CONTAINS);
    return Py_SAFE_DOWNCAST(result, Py_ssize_t, int);
}

タプルは次のように定義されていることに注意してください。

static PySequenceMethods tuple_as_sequence = {
    ...
    (objobjproc)tuplecontains,                  /* sq_contains */
};

PyTypeObject PyTuple_Type = {
    ...
    &tuple_as_sequence,                         /* tp_as_sequence */
    ...
};

だからブランチ

if (sqm != NULL && sqm->sq_contains != NULL)

が取られ*sqm->sq_contains、関数(objobjproc)tuplecontainsであるが取られます。

これは

static int
tuplecontains(PyTupleObject *a, PyObject *el)
{
    Py_ssize_t i;
    int cmp;

    for (i = 0, cmp = 0 ; cmp == 0 && i < Py_SIZE(a); ++i)
        cmp = PyObject_RichCompareBool(el, PyTuple_GET_ITEM(a, i),
                                           Py_EQ);
    return cmp;
}

...待っPyObject_RichCompareBoolて、それは他のブランチが取ったものではなかったのですか?いいえ、それはそうでしたPyObject_RichCompare

そのコードパスは短かったので、おそらくこれら2つの速度に達します。比較してみましょう。

int
PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
{
    PyObject *res;
    int ok;

    /* Quick result when objects are the same.
       Guarantees that identity implies equality. */
    if (v == w) {
        if (op == Py_EQ)
            return 1;
        else if (op == Py_NE)
            return 0;
    }

    ...
}

のコードパスはPyObject_RichCompareBoolほとんどすぐに終了します。の場合PyObject_RichCompare

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyObject *res;

    assert(Py_LT <= op && op <= Py_GE);
    if (v == NULL || w == NULL) { ... }
    if (Py_EnterRecursiveCall(" in comparison"))
        return NULL;
    res = do_richcompare(v, w, op);
    Py_LeaveRecursiveCall();
    return res;
}

Py_EnterRecursiveCall/ Py_LeaveRecursiveCallコンボは、前のパスで撮影されていないが、これらは比較的迅速なマクロであることよインクリメントし、いくつかのグローバルをデクリメントした後、短絡。

do_richcompare します:

static PyObject *
do_richcompare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;
    int checked_reverse_op = 0;

    if (v->ob_type != w->ob_type && ...) { ... }
    if ((f = v->ob_type->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        ...
    }
    ...
}

これは、コールにいくつかの簡単なチェックを行いますv->ob_type->tp_richcompareです

PyTypeObject PyUnicode_Type = {
    ...
    PyUnicode_RichCompare,      /* tp_richcompare */
    ...
};

する

PyObject *
PyUnicode_RichCompare(PyObject *left, PyObject *right, int op)
{
    int result;
    PyObject *v;

    if (!PyUnicode_Check(left) || !PyUnicode_Check(right))
        Py_RETURN_NOTIMPLEMENTED;

    if (PyUnicode_READY(left) == -1 ||
        PyUnicode_READY(right) == -1)
        return NULL;

    if (left == right) {
        switch (op) {
        case Py_EQ:
        case Py_LE:
        case Py_GE:
            /* a string is equal to itself */
            v = Py_True;
            break;
        case Py_NE:
        case Py_LT:
        case Py_GT:
            v = Py_False;
            break;
        default:
            ...
        }
    }
    else if (...) { ... }
    else { ...}
    Py_INCREF(v);
    return v;
}

つまり、これはleft == right...のショートカットですが、実行した後にのみ

    if (!PyUnicode_Check(left) || !PyUnicode_Check(right))

    if (PyUnicode_READY(left) == -1 ||
        PyUnicode_READY(right) == -1)

すべてのパスのすべては、このようになります(手動で再帰的にインライン化、既知のブランチの展開、プルーニング)

POP()                           # Stack stuff
TOP()                           #
                                #
case PyCmp_IN:                  # Dispatch on operation
                                #
sqm != NULL                     # Dispatch to builtin op
sqm->sq_contains != NULL        #
*sqm->sq_contains               #
                                #
cmp == 0                        # Do comparison in loop
i < Py_SIZE(a)                  #
v == w                          #
op == Py_EQ                     #
++i                             # 
cmp == 0                        #
                                #
res < 0                         # Convert to Python-space
res ? Py_True : Py_False        #
Py_INCREF(v)                    #
                                #
Py_DECREF(left)                 # Stack stuff
Py_DECREF(right)                #
SET_TOP(res)                    #
res == NULL                     #
DISPATCH()                      #

POP()                           # Stack stuff
TOP()                           #
                                #
default:                        # Dispatch on operation
                                #
Py_LT <= op                     # Checking operation
op <= Py_GE                     #
v == NULL                       #
w == NULL                       #
Py_EnterRecursiveCall(...)      # Recursive check
                                #
v->ob_type != w->ob_type        # More operation checks
f = v->ob_type->tp_richcompare  # Dispatch to builtin op
f != NULL                       #
                                #
!PyUnicode_Check(left)          # ...More checks
!PyUnicode_Check(right))        #
PyUnicode_READY(left) == -1     #
PyUnicode_READY(right) == -1    #
left == right                   # Finally, doing comparison
case Py_EQ:                     # Immediately short circuit
Py_INCREF(v);                   #
                                #
res != Py_NotImplemented        #
                                #
Py_LeaveRecursiveCall()         # Recursive check
                                #
Py_DECREF(left)                 # Stack stuff
Py_DECREF(right)                #
SET_TOP(res)                    #
res == NULL                     #
DISPATCH()                      #

今、PyUnicode_CheckそしてPyUnicode_READY、彼らはフィールドだけのカップルをチェックするので、かなり安いですが、トップ1が小さいコードパスであることは明らかである必要があり、それは少数の関数呼び出し、唯一のswitch文を持っており、ちょうどビット薄くなっています。

TL; DR:

両方に発送しif (left_pointer == right_pointer)ます。違いは、そこにたどり着くまでにどれだけの労力を要するかです。in少ないです。


18
これは信じられないほどの答えです。Pythonプロジェクトとの関係は何ですか?
kdbanman 2015年

9
@kdbanmanなし、本当に、私は少し自分の道強制することができましたが;)
Veedrac 2015年

21
@varepsilon Aww、でも実際の投稿をざっと見回す人はいません!質問のポイントは本当に答えではなくするために使用されるプロセスに着く答えは-うまくいけば生産でこのハックを使用している人々のトンがあるように起こっていません!
Veedrac 2015年

181

ここでは、組み合わさってこの驚くべき振る舞いを生み出す3つの要因があります。

最初に、in演算子はショートカットを取り、同一性(x is y)をチェックする前にID()をチェックしますx == y

>>> n = float('nan')
>>> n in (n, )
True
>>> n == n
False
>>> n is n
True

2番目:Pythonの文字列インターンのため、両方"x"のは"x" in ("x", )同じになります。

>>> "x" is "x"
True

(大きな警告:これは実装固有の動作です!文字列を比較するために使用しないisください。これは、時々驚くべき答え与えるためです。たとえば)"x" * 100 is "x" * 100 ==> False

第三:で詳述するようにVeedracの素晴らしい答えtuple.__contains__x in (y, )あるおよそに相当(y, ).__contains__(x))よりも速くIDチェックを実行する点に到達するstr.__eq__(再び、x == yあるおよそに相当x.__eq__(y))ありません。

これx in (y, )は、論理的に同等のものよりもかなり遅いため、この証拠を見ることができますx == y

In [18]: %timeit 'x' in ('x', )
10000000 loops, best of 3: 65.2 ns per loop

In [19]: %timeit 'x' == 'x'    
10000000 loops, best of 3: 68 ns per loop

In [20]: %timeit 'x' in ('y', ) 
10000000 loops, best of 3: 73.4 ns per loop

In [21]: %timeit 'x' == 'y'    
10000000 loops, best of 3: 56.2 ns per loop

x in (y, )後のでケースが遅くなるis比較が失敗した場合、inオペレータは正常に戻っ等価チェック(すなわち、使用に落ちる==ように比較は同じ時間程度かかりように)==ためのタプルを作成するオーバーヘッド全体の動作を遅くレンダリング、そのメンバーを歩くなど

また、ノートa in (b, )であるだけ速いときa is b

In [48]: a = 1             

In [49]: b = 2

In [50]: %timeit a is a or a == a
10000000 loops, best of 3: 95.1 ns per loop

In [51]: %timeit a in (a, )      
10000000 loops, best of 3: 140 ns per loop

In [52]: %timeit a is b or a == b
10000000 loops, best of 3: 177 ns per loop

In [53]: %timeit a in (b, )      
10000000 loops, best of 3: 169 ns per loop

(なぜこれa in (b, )より速いのですa is b or a == bか?私の推測では、仮想マシンの命令a in (b, )は少なくなると思います—  たった3つの命令で、a is b or a == bかなり多くのVM命令になります)

Veedracの答え- https://stackoverflow.com/a/28889838/71522は -のそれぞれの間に起こる、具体的内容にはるかに詳細に入る==in、リードも価値があります。


3
その理由は、これができるようにする可能性があるんX in [X,Y,Z]なしで正しく動作するXYまたはZ平等のメソッドを定義する必要が(というか、デフォルトの平等があるis、それが呼び出す必要がなくなりますので、__eq__ノーユーザ定義でのオブジェクトに__eq__し、is真であることが暗示すべき値-平等)。
aruisdante 2015年

1
の使用float('nan')は誤解を招く可能性があります。それはnanそれ自体と等しくないという特性です。それは可能タイミングを変更します。
dawg 2015年

@dawgああ、良い点— nanの例はin、メンバーシップテストの近道を説明するためだけのものでした。わかりやすくするために変数名を変更します。
David Wolever 2015年

3
私が理解している限り、CPython 3.4.3 tuple.__contains__で実装されtuplecontainsているのはPyObject_RichCompareBool、identityの場合にwhich呼び出しとそれがすぐに戻ることです。unicode持っているPyUnicode_RichCompareアイデンティティのための同じショートカットを持っているボンネットの下。
クリスティアンCiupitu 2015年

3
それは"x" is "x"必ずしもそうなるとは限らないということですTrue'x' in ('x', )常にですがTrue、それよりも高速に表示されない場合があり==ます。
David Wolever
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.