为什么'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
各地使用in
而不是==
!
正如我对David Wolever所说的那样,除此之外还有更多。 两种方法派遣到; 你可以通过这个来certificate这一点
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
第一个只能是如此之快,因为它通过身份检查。
为了找出为什么要比别人花更长的时间,让我们追踪执行。
它们都是从COMPARE_OP
开始的ceval.c
,因为这是涉及的字节码
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(); }
这将popup堆栈中的值(从技术上说,它只popup一个)
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; }
这是path分裂的地方。 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
。
这段代码path很短,所以它可能就是这两者的速度。 我们来比较一下。
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
的代码path几乎立即终止。 对于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
组合不在前面的path中,但是这些相对较快的macros会在增加和减less一些全局variables之后短路。
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
这个快捷键left == right
,但是只有在做完之后
if (!PyUnicode_Check(left) || !PyUnicode_Check(right)) if (PyUnicode_READY(left) == -1 || PyUnicode_READY(right) == -1)
所有的path然后看起来像这样(手动recursion内联,展开和修剪已知的分支)
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
是非常便宜的,因为它们只检查了几个字段,但是应该很明显,最上面的一个是较小的代码path,它具有较less的函数调用,只有一个switch语句,并且稍微更薄。
TL; DR:
都派遣到if (left_pointer == right_pointer)
; 不同的是他们做了多less工作才能到达那里。 in
less做。
这里有三个因素起作用,结合在一起产生这个令人惊讶的行为。
首先: in
运算符在检查相等性( x == y
)之前采用快捷方式并检查标识( x is y
x == y
):
>>> n = float('nan') >>> n in (n, ) True >>> n == n False >>> n is n True
第二:由于Python的string实习,两个"x"
"x" in ("x", )
将是相同的:
>>> "x" is "x" True
(大的警告:这是特定于实现的行为! 不应该用来比较string,因为它有时会给出令人惊讶的答案;例如"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, )
情况下的x in (y, )
比较慢,因为在比较失败之后, in
运算符回退到正常的相等性检查(即使用==
),所以比较花费的时间与==
相同整个操作由于创build元组,运行其成员等开销而变慢。
还要注意, 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
更快?我的猜测是虚拟机指令更less – a in (b, )
只有〜3个指令,其中a is b or a == b
将是相当多的虚拟机指令)
Veedrac的回答 – https://stackoverflow.com/a/28889838/71522 – 更具体地说明了每个==
和in
期间会发生什么in
并且非常值得一读。