为啥 ('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-&gt;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-&gt;ob_type-&gt;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_CheckPyUnicode_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] 能够正常工作,而无需XYZ 必须定义相等方法(或者更确切地说,默认相等是is,因此它不必在没有用户定义的对象上调用 __eq__ __eq__is 为真应该意味着价值平等)。 float('nan') 的使用可能具有误导性。 nan 的一个属性是它不等于它自己。这可能改变时间。 @dawg 啊,好点——这个 nan 示例只是为了说明快捷方式 in 进行成员资格测试。我将更改变量名称以澄清。 据我了解,在 CPython 3.4.3 中,tuple.__contains__ 由调用 PyObject_RichCompareBooltuplecontains 实现,并且在标识的情况下立即返回。 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++) 快? [复制]

为啥 np.linalg.norm(x,2) 比直接求解慢?

为啥这比函数有效?

对为啥我的算法运行速度比它应该的慢感到困惑

为啥 autoconf 脚本中的条件在变量前加上“x”?