为什么(‘ x’)中的‘ x’比‘ x’= = ‘ x’快?

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

同样适用于具有多个元素的元组,两个版本似乎都呈线性增长:

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

基于这一点,我认为我应该 绝对的开始使用 in而不是 ==的任何地方!

20456 次浏览

有三个因素在起作用,综合起来,产生了这种令人惊讶的行为。

首先:in操作符使用了一个快捷方式,在检查相等性(x == y)之前检查身份(x is y):

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

第二:由于Python的字符串实习,"x" in ("x", )中的两个__abc0将是相同的:

>>> "x" is "x"
True

(警告:这是特定于实现的行为!is应该用来比较字符串,因为它有时会给出令人惊讶的答案;例如"x" * 100 is "x" * 100 ==> False)

第三:正如Veedrac奇妙的回答中详细描述的那样,tuple.__contains__ (x in (y, )等价于(y, ).__contains__(x))比str.__eq__(同样,x == y等价于x.__eq__(y))更快地执行身份检查。

你可以看到这一点的证据,因为x in (y, )明显比逻辑上等价的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

x in (y, )的情况比较慢,因为在is比较失败后,in操作符返回到正常的相等性检查(即使用==),因此比较所花费的时间与==大致相同,导致整个操作变慢,因为创建元组、遍历其成员等开销。

还要注意,当a is b时,a in (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

(为什么a in (b, )a is b or a == b快?我猜会有更少的虚拟机指令- a in (b, )只有~3条指令,而a is b or a == b将有相当多的虚拟机指令)

Veedrac的答案——https://stackoverflow.com/a/28889838/71522——更详细地介绍了在==in期间具体发生了什么,非常值得一读。

正如我向David Wolever提到的那样,事情远比看上去的要复杂;两个方法都分派到is;你可以通过实践来证明这一点

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

第一种只能这么快,因为它是通过身份进行检查的。

为了找出为什么一个会比另一个花费更长的时间,让我们跟踪执行过程。

它们都从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,                  /* sq_contains */
};


PyTypeObject PyTuple_Type = {
...
&tuple_as_sequence,                         /* tp_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;


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


...
}

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,      /* tp_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:
/* 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;
}

也就是说,这个快捷键在left == right…但只有在

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


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

所有的路径看起来就像这样(手动递归内联,展开和修剪已知的分支)

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()                      #
现在,PyUnicode_CheckPyUnicode_READY非常便宜,因为它们只检查两个字段,但应该很明显,最上面的是一个更小的代码路径,它有更少的函数调用,只有一个开关 语句,只是稍微薄一点

TL;博士:

两者都分派到if (left_pointer == right_pointer);不同之处在于他们要做多少工作才能达到目标。in做得更少。