Perché 'x' in ('x',) è più veloce di 'x' == 'x'?


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

Funziona anche con tuple con più elementi, entrambe le versioni sembrano crescere in modo lineare:

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

Sulla base di questo, penso che dovrei assolutamente iniziare a utilizzare inovunque al posto di ==!


167
Per ogni evenienza: non iniziare a utilizzare inovunque invece di ==. È un'ottimizzazione prematura che danneggia la leggibilità.
Colonnello Thirty Two

4
prova x ="!foo" x in ("!foo",)ex == "!foo"
Padraic Cunningham il

2
A in B = Valore, C == D Confronto tra valore e tipo
dsgdfg

6
Un approccio più ragionevole dell'uso ininvece di ==è quello di passare a C.
Mad Physicist,

1
Se stai scrivendo in Python e scegli un costrutto piuttosto che un altro per la velocità, stai sbagliando.
Veky,

Risposte:


257

Come ho già detto a David Wolever, c'è di più di ciò che sembra; entrambi i metodi vengono spediti a is; puoi dimostrarlo facendo

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

Il primo non può che essere così veloce perché controlla per identità.

Per scoprire perché uno richiederebbe più tempo dell'altro, tracciamo attraverso l'esecuzione.

Entrambi iniziano ceval.c, dal COMPARE_OPmomento che è il bytecode coinvolto

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

Questo estrae i valori dallo stack (tecnicamente ne apre solo uno)

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

ed esegue il confronto:

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

cmp_outcome è questo:

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

Questo è dove i percorsi si dividono. Lo PyCmp_INfa il ramo

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

Si noti che una tupla è definita come

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

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

Quindi il ramo

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

sarà preso e *sqm->sq_contains, che è la funzione (objobjproc)tuplecontains, sarà preso.

Questo fa

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

... Aspetta, non è quello PyObject_RichCompareBoolche ha preso l'altra filiale? No, quello era PyObject_RichCompare.

Quel percorso del codice era breve, quindi probabilmente dipende solo dalla velocità di questi due. Facciamo un confronto.

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

    ...
}

Il percorso del codice in PyObject_RichCompareBoolpraticamente termina immediatamente. Perché PyObject_RichComparelo fa

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

La combo Py_EnterRecursiveCall/ Py_LeaveRecursiveCallnon è stata presa nel percorso precedente, ma si tratta di macro relativamente rapide che cortocircuiteranno dopo l'incremento e il decremento di alcuni globali.

do_richcompare fa:

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

Questo fa alcuni controlli rapidi per chiamare v->ob_type->tp_richcompareche è

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

che fa

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

Vale a dire, queste scorciatoie su left == right... ma solo dopo averlo fatto

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

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

Tutto sommato i percorsi allora assomigliano a questo (allineare ricorsivamente manualmente, srotolare e potare rami noti)

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

Ora, PyUnicode_Checke PyUnicode_READYsono piuttosto economici poiché controllano solo un paio di campi, ma dovrebbe essere ovvio che quello in alto è un percorso di codice più piccolo, ha meno chiamate di funzione, solo un'istruzione switch ed è solo un po 'più sottile.

TL; DR:

Entrambi inviano a if (left_pointer == right_pointer); la differenza è solo quanto lavoro fanno per arrivarci. infa solo di meno.


18
Questa è una risposta incredibile. Qual è la tua relazione con il progetto Python?
kdbanman,

9
@kdbanman Nessuno, davvero, anche se sono riuscito a farmi strada un po ';).
Veedrac,

21
@varepsilon Aww, ma nessuno si preoccuperebbe di sfogliare il post vero e proprio! Il punto della domanda non è proprio la risposta ma il processo usato per arrivare alla risposta - si spera che non ci saranno un sacco di persone che usano questo hack in produzione!
Veedrac,

181

Ci sono tre fattori in gioco qui che, combinati, producono questo comportamento sorprendente.

Primo: l' inoperatore prende una scorciatoia e controlla l'identità ( x is y) prima di controllare l'uguaglianza ( x == y):

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

Secondo: a causa dell'interning di stringhe di Python, entrambi gli "x"s in "x" in ("x", )saranno identici:

>>> "x" is "x"
True

(grande attenzione: questo è un comportamento implementazione specifico! isdovrebbe mai essere utilizzato per confrontare le stringhe, perché vi darà risposte sorprendenti a volte, ad esempio "x" * 100 is "x" * 100 ==> False)

Terzo: come dettagliato nella fantastica risposta di Veedrac , tuple.__contains__( x in (y, )è approssimativamente equivalente a (y, ).__contains__(x)) arriva al punto di eseguire il controllo dell'identità più velocemente di str.__eq__(di nuovo, x == yè approssimativamente equivalente a x.__eq__(y)).

Puoi vedere le prove per questo perché x in (y, )è significativamente più lento dell'equivalente logicamente 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

Il x in (y, )caso è più lento perché, dopo il isfallimento del confronto, l' inoperatore ritorna al normale controllo di uguaglianza (cioè utilizzando ==), quindi il confronto richiede lo stesso tempo ==, rendendo l'intera operazione più lenta a causa del sovraccarico di creazione della tupla , camminando sui suoi membri, ecc.

Nota anche che a in (b, )è più veloce solo quando 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

(perché è a in (b, )più veloce di a is b or a == b? La mia ipotesi sarebbe un minor numero di istruzioni della macchina virtuale -  a in (b, )sono solo ~ 3 istruzioni, dove a is b or a == bci saranno molte più istruzioni della VM)

La risposta di Veedrac - https://stackoverflow.com/a/28889838/71522 - fornisce ulteriori dettagli su ciò che accade durante ciascuno di essi ==e invale la pena leggerlo.


3
E la ragione per cui lo fa è probabile che permetta X in [X,Y,Z]di funzionare correttamente senza X, Yo Zdover definire metodi di uguaglianza (o meglio, l'uguaglianza di default è is, quindi risparmia di dover fare ricorso __eq__a oggetti senza definito dall'utente __eq__ed isessere vero dovrebbe implicare valore -uguaglianza).
Aruisdante,

1
L'uso di float('nan')è potenzialmente fuorviante. È una proprietà di nanciò che non è uguale a se stesso. Ciò potrebbe cambiare i tempi.
dawg

@dawg ah, buon punto - l'esempio di nan aveva lo scopo di illustrare le scelte rapide insui test di appartenenza. Cambierò il nome della variabile per chiarire.
David Wolever,

3
Per quanto ho capito, in CPython 3.4.3 tuple.__contains__è implementato il tuplecontainsquale chiama PyObject_RichCompareBoole che ritorna immediatamente in caso di identità. unicodeha PyUnicode_RichComparesotto il cofano, che ha la stessa scorciatoia per l'identità.
Cristian Ciupitu,

3
Significa che "x" is "x"non lo sarà necessariamente True. 'x' in ('x', )lo sarà sempre True, ma potrebbe non sembrare più veloce di ==.
David Wolever,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.