Mar*_*nen 273 python performance python-3.x python-internals
>>> timeit.timeit("'x' in ('x',)")
0.04869917374131205
>>> timeit.timeit("'x' == 'x'")
0.06144205736110564
Run Code Online (Sandbox Code Playgroud)
也适用于具有多个元素的元组,两个版本似乎线性增长:
>>> 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
Run Code Online (Sandbox Code Playgroud)
基于此,我认为我应该完全开始使用in到处而不是==!
Vee*_*rac 256
正如我提到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
Run Code Online (Sandbox Code Playgroud)
第一个只能这么快,因为它按身份检查.
为了找出为什么人们需要比另一个更长的时间,让我们追溯执行.
它们都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();
}
Run Code Online (Sandbox Code Playgroud)
这会弹出堆栈中的值(从技术上讲,它只会弹出一个)
PyObject *right = POP();
PyObject *left = TOP();
Run Code Online (Sandbox Code Playgroud)
并运行比较:
PyObject *res = cmp_outcome(oparg, left, right);
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
这是路径分裂的地方.该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);
}
Run Code Online (Sandbox Code Playgroud)
请注意,元组定义为
static PySequenceMethods tuple_as_sequence = {
...
(objobjproc)tuplecontains, /* sq_contains */
};
PyTypeObject PyTuple_Type = {
...
&tuple_as_sequence, /* tp_as_sequence */
...
};
Run Code Online (Sandbox Code Playgroud)
所以分支
if (sqm != NULL && sqm->sq_contains != NULL)
Run Code Online (Sandbox Code Playgroud)
将被采取*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;
}
Run Code Online (Sandbox Code Playgroud)
......等等,是不是那个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;
}
...
}
Run Code Online (Sandbox Code Playgroud)
代码路径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;
}
Run Code Online (Sandbox Code Playgroud)
将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;
...
}
...
}
Run Code Online (Sandbox Code Playgroud)
这会做一些快速检查来调用v->ob_type->tp_richcompare它
PyTypeObject PyUnicode_Type = {
...
PyUnicode_RichCompare, /* tp_richcompare */
...
};
Run Code Online (Sandbox Code Playgroud)
哪个
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;
}
Run Code Online (Sandbox Code Playgroud)
也就是说,这个快捷方式left == right......但仅限于此之后
if (!PyUnicode_Check(left) || !PyUnicode_Check(right))
if (PyUnicode_READY(left) == -1 ||
PyUnicode_READY(right) == -1)
Run Code Online (Sandbox Code Playgroud)
总而言之,这些路径看起来像这样(手动递归内联,展开和修剪已知分支)
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() #
Run Code Online (Sandbox Code Playgroud)
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() #
Run Code Online (Sandbox Code Playgroud)
现在,PyUnicode_Check和PyUnicode_READY相当便宜,因为他们只检查了几个领域,但它应该是显而易见的是,上面一个是较小的代码路径,它具有较少的函数调用,只有一个开关语句是只是有点薄.
两者都派遣到if (left_pointer == right_pointer); 不同之处在于他们为实现这一目标所做的工作量.in只是少做.
Dav*_*ver 181
这里有三个因素共同产生了这种令人惊讶的行为.
第一:in运算符x is y在检查equality(x == y)之前采用快捷方式并检查identity ():
>>> n = float('nan')
>>> n in (n, )
True
>>> n == n
False
>>> n is n
True
Run Code Online (Sandbox Code Playgroud)
第二:由于Python的字符串实习,两个"x"s "x" in ("x", )将是相同的:
>>> "x" is "x"
True
Run Code Online (Sandbox Code Playgroud)
(大警告:这是实现特定的行为!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
Run Code Online (Sandbox Code Playgroud)
这种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
Run Code Online (Sandbox Code Playgroud)
(为什么a in (b, )比这更快a is b or a == b?我的猜测是虚拟机指令会更少 - a in (b, )只有3条指令,其中a is b or a == b会有更多的VM指令)
Veedrac的回答 - /sf/answers/2022288691/ - 详细介绍了每一个期间发生的事情==,in并且非常值得一读.
| 归档时间: |
|
| 查看次数: |
18405 次 |
| 最近记录: |