>>> 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,而不是==!
发布于 2015-03-05 23:29:11
正如我向David提到的那样,这不仅仅是眼见为实;这两种方法都分派给了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;
}...Wait,那不是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_Check和PyUnicode_READY非常便宜,因为它们只检查几个字段,但是应该很明显,最上面的字段是一个较小的代码路径,它有较少的函数调用,只有一个switch语句,只是比较薄一点。
TL;DR:
两者都分派给了if (left_pointer == right_pointer);区别在于他们要做多少工作才能到达那里。in只是做得更少。
发布于 2015-03-05 18:35:06
这里有三个因素,综合起来,产生了这种令人惊讶的行为。
首先:in操作符采用快捷方式,在检查等式(x == y)之前检查标识(x is y):
>>> n = float('nan')
>>> n in (n, )
True
>>> n == n
False
>>> n is n
True第二:由于Python的字符串实习,"x" in ("x", )中的两个"x" in ("x", )将是相同的:
>>> "x" is "x"
True(大警告:这是特定于实现的行为!is不应该用于比较字符串,因为它有时会给出令人惊讶的答案;例如,"x" * 100 is "x" * 100 ==> False)
第三:正如Veedrac's fantastic answer中详细介绍的那样,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 loopx in (y, )的情况比较慢,因为在is比较失败后,in操作符返回到正常的等式检查(即使用==),因此比较所需的时间大约与==相同,因为创建元组、遍历其成员等开销使整个操作变慢。
还请注意,a in (b, )只有在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(为什么a in (b, )比a is b or a == b快?我的猜测是虚拟机指令- a in (b, )只有~3条指令,其中a is b or a == b将是更多的VM指令)
Veedrac的答案-- https://stackoverflow.com/a/28889838/71522 --更详细地说明了在==和in的每一个过程中发生了什么,这是非常值得一读的。
https://stackoverflow.com/questions/28885132
复制相似问题