为啥 ('x',) 中的 'x' 比 'x' == 'x' 快?
Posted
技术标签:
【中文标题】为啥 (\'x\',) 中的 \'x\' 比 \'x\' == \'x\' 快?【英文标题】:Why is 'x' in ('x',) faster than 'x' == 'x'?为什么 ('x',) 中的 'x' 比 'x' == 'x' 快? 【发布时间】:2015-05-07 06:03:01 【问题描述】:>>> 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
,而不是==
!
【问题讨论】:
以防万一:请不要在任何地方开始使用in
而不是==
。这是一种过早的优化,会损害可读性。
试试x ="!foo"
x in ("!foo",)
和x == "!foo"
A in B = Value , C == D Value and Type 比较
比使用in
而不是==
更合理的做法是切换到C。
如果您使用 Python 编写代码,并且为了速度而选择一种结构而不是另一种结构,那么您做错了。
【参考方案1】:
正如我向 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() #
对
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
非常便宜,因为它们只检查几个字段,但很明显,最上面的代码路径更小,函数调用更少,只有一个开关
声明,只是有点薄。
TL;DR:
都派发到if (left_pointer == right_pointer)
;不同之处在于他们为到达那里做了多少工作。 in
做得更少。
【讨论】:
这是一个令人难以置信的答案。你和python项目是什么关系? @kdbanman 没有,真的,虽然我已经设法force my way in 一点;)。 @varepsilon Aww,但是没有人会费心浏览实际的帖子!问题的重点不是真正的答案,而是用于得到答案的过程 - 希望不会有很多人在生产中使用这个 hack!【参考方案2】:这里有三个因素在起作用,它们共同产生了这种令人惊讶的行为。
首先: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"
s 将是相同的:
>>> "x" is "x"
True
(严重警告:这是特定于实现的行为!is
不应该用于比较字符串,因为它会有时会给出令人惊讶的答案;例如@987654334 @)
第三:如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 loop
x 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://***.com/a/28889838/71522 — 更详细地说明了在 ==
和 in
期间发生的具体情况,非常值得一读。
【讨论】:
它这样做的原因可能是让X in [X,Y,Z]
能够正常工作,而无需X
、Y
或Z
必须定义相等方法(或者更确切地说,默认相等是is
,因此它不必在没有用户定义的对象上调用 __eq__
__eq__
和 is
为真应该意味着价值平等)。
float('nan')
的使用可能具有误导性。 nan
的一个属性是它不等于它自己。这可能改变时间。
@dawg 啊,好点——这个 nan 示例只是为了说明快捷方式 in
进行成员资格测试。我将更改变量名称以澄清。
据我了解,在 CPython 3.4.3 中,tuple.__contains__
由调用 PyObject_RichCompareBool
的 tuplecontains
实现,并且在标识的情况下立即返回。 unicode
在引擎盖下有 PyUnicode_RichCompare
,它具有相同的标识快捷方式。
这意味着"x" is "x"
不一定是True
。 'x' in ('x', )
将始终为 True
,但它可能不会比 ==
快。以上是关于为啥 ('x',) 中的 'x' 比 'x' == 'x' 快?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 SSE scalar sqrt(x) 比 rsqrt(x) * x 慢?
为啥在 C 中前缀递增 (++x) 比后缀递增 (x++) 快? [复制]