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.