web-dev-qa-db-fra.com

Pourquoi "x" dans ("x",) est-il plus rapide que "x" == "x"?

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

Fonctionne également pour les n-uplets à plusieurs éléments, les deux versions semblent se développer linéairement:

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

Sur cette base, je pense que je devrais totalement commencer à utiliser in partout au lieu de ==!

272
Markus Meskanen

Comme je l'ai mentionné à David Wolever, il y a plus que cela à l'œil nu; les deux méthodes envoient à is; vous pouvez le prouver en faisant

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

Le premier ne peut être aussi rapide que parce qu'il vérifie par identité.

Pour savoir pourquoi l’un prend plus de temps que l’autre, suivons l’exécution.

Ils commencent tous deux par ceval.c, À partir de COMPARE_OP, Puisque c'est le bytecode impliqué.

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

Cela extrait les valeurs de la pile (techniquement, il n'en saute qu'une)

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

et exécute la comparaison:

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

cmp_outcome Est la suivante:

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

C'est là que les chemins se séparent. La branche PyCmp_IN Fait

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

Notez qu'un tuple est défini comme

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

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

Donc la branche

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

sera prise et *sqm->sq_contains, qui est la fonction (objobjproc)tuplecontains, sera prise.

Cela fait

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

... Attendez, n'est-ce pas PyObject_RichCompareBool Ce que l'autre branche a pris? Non, c'était PyObject_RichCompare.

Ce chemin de code était court, de sorte qu'il est probablement réduit à la vitesse de ces deux-là. Comparons.

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

    ...
}

Le chemin de code dans PyObject_RichCompareBool Se termine presque immédiatement. Pour PyObject_RichCompare, Cela signifie

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

Les combos Py_EnterRecursiveCall/Py_LeaveRecursiveCall Ne sont pas pris dans le chemin précédent, mais ce sont des macros relativement rapides qui court-circuitent après avoir incrémenté et décrémenté des globaux.

do_richcompare Fait:

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

Ceci fait quelques vérifications rapides pour appeler v->ob_type->tp_richcompare Qui est

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

qui fait

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

À savoir, ces raccourcis sur left == right ... mais seulement après avoir

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

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

Dans l’ensemble, les chemins ressemblent à quelque chose comme ceci (aligner, dérouler et élaguer manuellement de manière récursive des branches connues)

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

contre

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

Maintenant, PyUnicode_Check Et PyUnicode_READY Sont assez économiques puisqu'ils ne vérifient que quelques champs, mais il devrait être évident que le premier est un chemin de code plus petit, il comporte moins d'appels de fonction déclaration de commutateur et est juste un peu plus mince.

TL; DR:

Les deux envoient à if (left_pointer == right_pointer); la différence est juste combien de travail ils font pour y arriver. in fait juste moins.

254
Veedrac

Il y a trois facteurs en jeu qui, combinés, produisent ce comportement surprenant.

Premièrement: l'opérateur in prend un raccourci et vérifie l'identité (x is y) Avant de vérifier l'égalité (x == y):

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

Deuxièmement: à cause de l'internation de la chaîne de Python, les deux "x" Dans "x" in ("x", ) seront identiques:

>>> "x" is "x"
True

(grand avertissement: il s’agit d’un comportement spécifique à l’implémentation! is ne devrait jamais être utilisé pour comparer des chaînes car il donnera des réponses parfois surprenantes, par exemple "x" * 100 is "x" * 100 ==> False)

Troisièmement: comme détaillé dans réponse fantastique de Veedrac , Tuple.__contains__ (x in (y, ) est à peu près L’équivalent de (y, ).__contains__(x)) en arrive à effectuer le contrôle d’identité plus rapidement que str.__eq__ (encore une fois, x == y est approximativement équivalent à x.__eq__(y)) fait.

Vous pouvez en voir la preuve car x in (y, ) est nettement plus lent que l'équivalent logique, 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

La casse x in (y, ) est plus lente car, après l'échec de la comparaison is, l'opérateur in revient à la vérification normale de l'égalité (c'est-à-dire, en utilisant ==), la comparaison prend donc à peu près le même temps que ==, ce qui ralentit toute l'opération en raison de la surcharge liée à la création du tuple, à la marche de ses membres, etc.

Notez également que a in (b, ) est seulement plus rapidement lorsque 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

(pourquoi a in (b, ) est-il plus rapide que a is b or a == b? Je suppose qu'il y aurait moins d'instructions de machine virtuelle - a in (b, ) ne contient que ~ 3 instructions, où a is b or a == b sera plusieurs autres VM instructions)

La réponse de Veedrac - https://stackoverflow.com/a/28889838/71522 - décrit plus en détail ce qui se passe au cours de chacun des == Et in et vaut bien la lecture.

181
David Wolever