Warum ist 'x' in ('x',) schneller als 'x' == 'x'?

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

Funktioniert auch für Tupel mit mehreren Elementen. Beide Versionen scheinen linear zu wachsen:

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

Aus diesem Grund denke ich, ich sollte total anfangen, inüberall zu verwenden, anstatt ==!

Markus Meskanen
quelle
167
Nur für den Fall: Bitte verwenden Sie nicht inüberall statt ==. Es ist eine vorzeitige Optimierung, die die Lesbarkeit beeinträchtigt.
Oberst
4
versuchen Sie x ="!foo" x in ("!foo",)undx == "!foo"
Padraic Cunningham
2
A in B = Wert, C == D
Wert-
6
Ein vernünftiger Ansatz als die Verwendung inanstelle von ==zu C wechseln
Mad Physikerin
1
Wenn Sie in Python schreiben und aus Gründen der Geschwindigkeit ein Konstrukt einem anderen vorziehen, machen Sie es falsch.
Veky

Antworten:

257

Wie ich David Wolever gegenüber erwähnte, steckt mehr dahinter, als man denkt. beide Methoden versenden an is; Sie können dies beweisen, indem Sie dies tun

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

Der erste kann nur so schnell sein, weil er nach Identität prüft.

Um herauszufinden, warum einer länger dauert als der andere, verfolgen wir die Ausführung.

Sie beginnen beide in ceval.c, COMPARE_OPda dies der Bytecode ist

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();
}

Dadurch werden die Werte aus dem Stapel entfernt (technisch gesehen wird nur einer angezeigt).

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

und führt den Vergleich aus:

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

cmp_outcome ist das:

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

Hier teilen sich die Pfade. Die PyCmp_INNiederlassung tut

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);
}

Beachten Sie, dass ein Tupel definiert ist als

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

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

Also der Zweig

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

wird genommen und *sqm->sq_contains, was die Funktion ist (objobjproc)tuplecontains, wird genommen.

Das macht

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

... Warten Sie, war es nicht das, PyObject_RichCompareBoolwas der andere Zweig genommen hat? Nein, das war PyObject_RichCompare.

Dieser Codepfad war kurz, daher kommt es wahrscheinlich nur auf die Geschwindigkeit dieser beiden an. Lass uns vergleichen.

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

    ...
}

Der Codepfad in wird so PyObject_RichCompareBoolziemlich sofort beendet. Denn PyObject_RichComparedas tut es

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

Die Py_EnterRecursiveCall/ Py_LeaveRecursiveCallCombo werden nicht im vorherigen Pfad verwendet, aber dies sind relativ schnelle Makros, die nach dem Inkrementieren und Dekrementieren einiger Globals kurzgeschlossen werden.

do_richcompare tut:

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;
        ...
    }
    ...
}

Dies hat einige schnelle Kontrolle zu Anruf , v->ob_type->tp_richcompareder ist

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

was tut

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

Diese Verknüpfungen werden nämlich aktiviert left == right... aber erst danach

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

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

Alles in allem sehen die Pfade dann ungefähr so ​​aus (manuelles rekursives Inlinen, Abrollen und Beschneiden bekannter Zweige)

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

vs.

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

Nun, PyUnicode_Checkund PyUnicode_READYsind ziemlich billig, da sie nur ein paar Felder prüfen, aber es sollte offensichtlich sein, dass das oberste ein kleinerer Codepfad ist, weniger Funktionsaufrufe, nur eine switch-Anweisung und nur etwas dünner ist.

TL; DR:

Beide versenden an if (left_pointer == right_pointer); Der Unterschied ist nur, wie viel Arbeit sie tun, um dorthin zu gelangen. inmacht einfach weniger.

Veedrac
quelle
18
Das ist eine unglaubliche Antwort. In welcher Beziehung stehen Sie zum Python-Projekt?
Kdbanman
9
@kdbanman Eigentlich keine, obwohl ich es geschafft habe , mich ein bisschen durchzusetzen ;).
Veedrac
21
@varepsilon Aww, aber dann würde sich niemand die Mühe machen, den eigentlichen Beitrag zu überfliegen! Der Punkt der Frage ist nicht wirklich die Antwort, aber der Prozess, der verwendet wurde, um zur Antwort zu gelangen - hoffentlich wird es nicht eine Menge Leute geben, die diesen Hack in der Produktion verwenden!
Veedrac
181

Hier spielen drei Faktoren eine Rolle, die zusammen dieses überraschende Verhalten hervorrufen.

Erstens: Der inOperator nimmt eine Verknüpfung und überprüft die Identität ( x is y), bevor er die Gleichheit überprüft ( x == y):

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

Zweitens: Aufgrund der Internierung von Pythons Zeichenfolgen sind beide "x"s in "x" in ("x", )identisch:

>>> "x" is "x"
True

(große Warnung: Diese ist implementierungsspezifisches Verhalten! issollte nie Strings verwendet werden , um zu vergleichen , denn es wird manchmal überraschende Antworten geben, zum Beispiel"x" * 100 is "x" * 100 ==> False )

Drittens: wie in detailliert Veedrac fantastischen Antwort , tuple.__contains__( x in (y, )ist in etwa äquivalent (y, ).__contains__(x)) auf den Punkt , die Identitätsprüfung schneller als die Durchführung str.__eq__(wiederum x == yist in etwa gleichwertig x.__eq__(y)) den Fall ist.

Sie können Beweise dafür sehen, weil sie x in (y, )erheblich langsamer sind als das logisch Äquivalent 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

Der x in (y, )Fall ist langsamer, da isder inBediener nach dem Fehlschlagen des Vergleichs auf die normale Gleichheitsprüfung zurückgreift (dh mit== ), sodass der Vergleich ungefähr genauso lange dauert wie ==der Vorgang, wodurch der gesamte Vorgang aufgrund des Overheads beim Erstellen des Tupels langsamer wird , zu Fuß seine Mitglieder, etc.

Beachten Sie auch , dass a in (b, )ist nur schneller , wenn 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

(Warum ist a in (b, )schneller als a is b or a == b? Meine Vermutung wären weniger Anweisungen für virtuelle Maschinen -  a in (b, )es sind nur ~ 3 Anweisungen, woa is b or a == b einige VM-Anweisungen mehr vorhanden sind.)

Die Antwort von Veedrac - https://stackoverflow.com/a/28889838/71522 - geht viel detaillierter darauf ein, was genau während ==und passiert, inund ist es wert, gelesen zu werden.

David Wolever
quelle
3
Und der Grund , es tut dies wahrscheinlich zu erlauben , X in [X,Y,Z]zu arbeiten korrekt , ohne X, Yoder Zmit Gleichheit Methoden definieren (oder besser gesagt, die Standard - Gleichheit ist is, so dass es mit spart rief __eq__auf Objekte ohne benutzerdefinierten __eq__und iswahr sein sollte Wert implizieren -Gleichberechtigung).
aruisdante
1
Die Verwendung von float('nan')ist möglicherweise irreführend. Es ist eine Eigenschaft davon, nandass es nicht gleich sich selbst ist. Das kann das Timing ändern.
Morgengrauen
@dawg ah, guter Punkt - das Nan-Beispiel sollte nur die Abkürzung infür Mitgliedschaftstests veranschaulichen . Ich werde den Variablennamen ändern, um dies zu verdeutlichen.
David Wolever
3
Soweit ich weiß, tuple.__contains__ist in CPython 3.4.3 implementiert, mit tuplecontainsdem Aufrufe PyObject_RichCompareBoolund die im Falle einer Identität sofort zurückkehren. unicodehat PyUnicode_RichCompareunter der Haube, die die gleiche Abkürzung für Identität hat.
Cristian Ciupitu
3
Das heißt, das "x" is "x"wird nicht unbedingt so sein True. 'x' in ('x', )wird immer sein True, aber es scheint nicht schneller zu sein als ==.
David Wolever