为什么'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_CheckPyUnicode_READY是非常便宜的,因为它们只检查了几个字段,但是应该很明显,最上面的一个是较小的代码path,它具有较less的函数调用,只有一个switch语句,并且稍微更薄。

TL; DR:

都派遣到if (left_pointer == right_pointer) ; 不同的是他们做了多less工作才能到达那里。 inless做。

这里有三个因素起作用,结合在一起产生这个令人惊讶的行为。

首先: 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并且非常值得一读。