Wie ich David Wolever gegenüber erwähnte, steckt mehr dahinter, als man denkt. beide Methoden versenden an is
; Sie können dies beweisen, indem Sie dies tun
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 prüft.
Um herauszufinden, warum einer länger dauert als der andere, verfolgen wir die Ausführung.
Sie beginnen beide in ceval.c
, 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 entfernt (technisch gesehen wird nur einer angezeigt).
PyObject *right = POP();
PyObject *left = TOP();
und führt den Vergleich aus:
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 Pfade. Die PyCmp_IN
Niederlassung tut
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 definiert ist als
static PySequenceMethods tuple_as_sequence = {
...
(objobjproc)tuplecontains, /* sq_contains */
};
PyTypeObject PyTuple_Type = {
...
&tuple_as_sequence, /* tp_as_sequence */
...
};
Also der Zweig
if (sqm != NULL && sqm->sq_contains != NULL)
wird genommen und *sqm->sq_contains
, was die Funktion ist (objobjproc)tuplecontains
, wird genommen.
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;
}
... Warten Sie, war es nicht das, PyObject_RichCompareBool
was der andere Zweig genommen hat? Nein, das war PyObject_RichCompare
.
Dieser Codepfad war kurz, daher kommt es wahrscheinlich nur auf die Geschwindigkeit dieser beiden an. 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 Codepfad in wird so PyObject_RichCompareBool
ziemlich sofort beendet. Denn PyObject_RichCompare
das tut 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
Combo werden nicht im vorherigen Pfad verwendet, aber dies sind relativ schnelle Makros, die nach dem Inkrementieren und Dekrementieren einiger Globals kurzgeschlossen werden.
do_richcompare
tut:
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 hat einige schnelle Kontrolle zu Anruf , v->ob_type->tp_richcompare
der ist
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;
}
Diese Verknüpfungen werden nämlich aktiviert left == right
... aber erst danach
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 (manuelles rekursives Inlinen, Abrollen und Beschneiden 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 prüfen, aber es sollte offensichtlich sein, dass das oberste ein kleinerer Codepfad ist, weniger Funktionsaufrufe, nur eine switch-Anweisung und nur etwas dünner ist.
TL; DR:
Beide versenden an if (left_pointer == right_pointer)
; Der Unterschied ist nur, wie viel Arbeit sie tun, um dorthin zu gelangen. in
macht einfach weniger.
in
überall statt==
. Es ist eine vorzeitige Optimierung, die die Lesbarkeit beeinträchtigt.