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





set axes (3)


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

Erstens: Der Operator in nimmt eine Abkürzung und prü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: Wegen Pythons String-Interning sind beide "x" s in "x" in ("x", ) identisch:

>>> "x" is "x"
True

(große Warnung: dies ist ein implementierungsspezifisches Verhalten!) sollte niemals zum Vergleichen von Strings verwendet werden, da es manchmal überraschende Antworten gibt; zum Beispiel "x" * 100 is "x" * 100 ==> False )

Drittens: tuple.__contains__ (wie in Veedracs fantastischer Antwort beschrieben) , tuple.__contains__ ( x in (y, ) ist ungefähr äquivalent zu (y, ).__contains__(x) ) erreicht den Punkt der Identitätsprüfung schneller als str.__eq__ x == y ist ungefähr äquivalent zu x.__eq__(y) ).

Sie können Beweise dafür sehen, weil x in (y, ) signifikant langsamer ist als das logisch äquivalente, 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 Fall x in (y, ) ist langsamer, da nach dem Fehlschlagen des Vergleichs der in Operator auf die normale Gleichheitsüberprüfung zurückfällt (dh mit == ), so dass der Vergleich ungefähr die gleiche Zeit wie == , Rendern benötigt die gesamte Operation langsamer wegen des Aufwands beim Erstellen des Tupels, Gehen seiner Mitglieder usw.

Beachten Sie auch, dass a in (b, ) nur schneller ist, wenn a 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 Annahme wäre weniger virtuelle Maschinenanweisungen - a in (b, ) ist nur ~ 3 Anweisungen, wobei a is b or a == b wird ein paar mehr VM-Anweisungen sein)

Veedrac's Antwort - https://stackoverflow.com/a/28889838/71522 - geht sehr viel detaillierter auf das was passiert während jedes == und in und ist das Lesen wert.

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

Darauf aufbauend sollte ich anfangen, überall statt == !




Wie ich David Wolever gegenüber erwähnt habe, gibt es da mehr, als man sieht; beide Methoden versenden nach; Das kannst du beweisen

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

Um herauszufinden, warum man länger braucht als die andere, lassen Sie uns die Ausführung verfolgen.

Beide starten in ceval.c von COMPARE_OP da 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 (technisch erscheint nur eins)

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

und führt den Vergleich durch:

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 Wege. Der PyCmp_IN Zweig tut dies

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 als definiert ist

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

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

So der Zweig

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

wird genommen und *sqm->sq_contains , welches die Funktion (objobjproc)tuplecontains , wird übernommen.

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

... Warte, war das nicht PyObject_RichCompareBool was der andere Zweig genommen hat? Nein, das war PyObject_RichCompare .

Dieser Code-Pfad war kurz, so dass es wahrscheinlich nur auf die Geschwindigkeit der beiden ankommt. 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 PyObject_RichCompareBool in PyObject_RichCompareBool ziemlich sofort beendet. Für PyObject_RichCompare ist 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_LeaveRecursiveCall wird nicht im vorherigen Pfad verwendet, aber es handelt sich um relativ schnelle Makros, die kurzgeschlossen werden, nachdem einige Globals inkrementiert und dekrementiert wurden.

do_richcompare macht:

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 führt einige schnelle Überprüfungen durch, um v->ob_type->tp_richcompare

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

Nämlich diese Shortcuts auf der left == right ... aber erst nach dem Ausführen

    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 (manuelle Rekursiveingliederung, -abrollung und -verkleinerung 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_Check und PyUnicode_READY sind ziemlich billig, da sie nur ein paar Felder überprüfen, aber es sollte offensichtlich sein, dass das oberste ein kleinerer PyUnicode_READY ist, es hat weniger Funktionsaufrufe, nur eine Switch-Anweisung und ist nur ein bisschen dünner.

TL; DR:

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




Die anderen Antworten haben es bereits gut erklärt, aber ich möchte ein weiteres Experiment anbieten, das die Art von Bereichsobjekten veranschaulicht:

>>> r = range(5)
>>> for i in r:
        print(i, 2 in r, list(r))

0 True [0, 1, 2, 3, 4]
1 True [0, 1, 2, 3, 4]
2 True [0, 1, 2, 3, 4]
3 True [0, 1, 2, 3, 4]
4 True [0, 1, 2, 3, 4]

Wie Sie sehen können, handelt es sich bei einem Bereichsobjekt um ein Objekt, das sich an seinen Bereich erinnert und viele Male (auch während des Iterierens) verwendet werden kann, nicht nur ein einmaliger Generator.





python performance python-3.x python-internals