>>> 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 ==
!
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.
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.
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.