正如我对David Wolever提到的那样,这个问题并不像表面看起来的那么简单;这两种方法都使用了is
(是)函数;您可以通过执行以下操作来证明:
min(Timer("x == x", setup="x = 'a' * 1000000").repeat(10, 10000))
min(Timer("x == y", setup="x = 'a' * 1000000; y = 'a' * 1000000").repeat(10, 10000))
第一个只能这么快,因为它按标识检查。
要找出为什么一个比另一个需要更长时间,让我们跟踪执行。
它们都从ceval.c
开始,在COMPARE_OP
中启动,因为那是涉及到的字节码。
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();
}
这会从堆栈中弹出值(严格来说,它只会弹出一个值)
PyObject *right = POP();
PyObject *left = TOP();
并运行比较:
PyObject *res = cmp_outcome(oparg, left, right);
cmp_outcome
是这样的:
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;
}
这是路径分叉的地方。 PyCmp_IN
分支执行
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);
}
请注意,元组的定义为:
static PySequenceMethods tuple_as_sequence = {
...
(objobjproc)tuplecontains,
};
PyTypeObject PyTuple_Type = {
...
&tuple_as_sequence,
...
};
因此,分支
if (sqm != NULL && sqm->sq_contains != NULL)
将会调用
*sqm->sq_contains
函数,该函数是
(objobjproc)tuplecontains
函数。
这样做会。
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;
}
等等,另一个分支使用的不是PyObject_RichCompareBool
吗?不是,那是PyObject_RichCompare
。
那段代码很短,所以很可能只是这两者速度的比较。让我们来比较一下。
int
PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
{
PyObject *res;
int ok;
if (v == w) {
if (op == Py_EQ)
return 1;
else if (op == Py_NE)
return 0;
}
...
}
PyObject_RichCompareBool
中的代码路径几乎立即终止。 对于 PyObject_RichCompare
,它则不是这样。
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;
}
Py_EnterRecursiveCall
/Py_LeaveRecursiveCall
组合在上一个路径中并未触发,但这些都是比较快的宏,在递增和递减一些全局变量后就会短路。
do_richcompare
的作用为:
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;
...
}
...
}
这会进行一些快速检查,调用v->ob_type->tp_richcompare
,这个函数是
PyTypeObject PyUnicode_Type = {
...
PyUnicode_RichCompare,
...
};
它执行的功能是
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:
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;
}
换句话说,这个快捷方式只适用于
left == right
……但在执行完以下操作后才适用。
if (!PyUnicode_Check(left) || !PyUnicode_Check(right))
if (PyUnicode_READY(left) == -1 ||
PyUnicode_READY(right) == -1)
总之,这些路径看起来大致如下(手动递归地进行内联、展开和修剪已知的分支)。
POP()
TOP()
case PyCmp_IN:
sqm != NULL
sqm->sq_contains != NULL
*sqm->sq_contains
cmp == 0
i < Py_SIZE(a)
v == w
op == Py_EQ
++i
cmp == 0
res < 0
res ? Py_True : Py_False
Py_INCREF(v)
Py_DECREF(left)
Py_DECREF(right)
SET_TOP(res)
res == NULL
DISPATCH()
vs
POP()
TOP()
default:
Py_LT <= op
op <= Py_GE
v == NULL
w == NULL
Py_EnterRecursiveCall(...)
v->ob_type != w->ob_type
f = v->ob_type->tp_richcompare
f != NULL
!PyUnicode_Check(left)
!PyUnicode_Check(right))
PyUnicode_READY(left) == -1
PyUnicode_READY(right) == -1
left == right
case Py_EQ:
Py_INCREF(v);
res != Py_NotImplemented
Py_LeaveRecursiveCall()
Py_DECREF(left)
Py_DECREF(right)
SET_TOP(res)
res == NULL
DISPATCH()
现在,PyUnicode_Check
和PyUnicode_READY
非常便宜,因为它们只检查了几个字段,但显然顶部的那个是更小的代码路径,它具有较少的函数调用,只有一个switch语句,并且只是略微细一点。
TL;DR:
两者都会分派到if(left_pointer == right_pointer)
;区别在于它们到达该点需要做的工作量。 in
只是做得更少。
in
取代==
。这是一种过早的优化,有损可读性。 - Colonel Thirty Twox ="!foo"
x in ("!foo",)
和x == "!foo"
- Padraic Cunninghamin
替代==
更为合理。 - Mad Physicist