为什么('x',)中的'x'比'x'=='x'快?


274
>>> 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在任何地方而不是在所有地方使用==


167
以防万一:请不要in在任何地方开始使用而不是==。这是过早的优化,会损害可读性。
三十二上校” 2015年

4
尝试 x ="!foo" x in ("!foo",)x == "!foo"
Padraic Cunningham 2015年

2
A in B =值,C == D值和类型比较
dsgdfg

6
更合理的方法比使用in代替==是切换到C.
疯狂物理学家

1
如果您使用Python编写并且为了提高速度而选择一个结构而不是另一个,则说明您做错了。
Veky

Answers:


257

正如我对大卫·沃尔沃(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.cCOMPARE_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_CheckPyUnicode_READY相当便宜,因为他们只检查了几个领域,但它应该是显而易见的是,上面一个是较小的代码路径,它具有较少的函数调用,只有一个开关语句是只是有点薄。

TL; DR:

都派往if (left_pointer == right_pointer); 不同之处在于他们为达到目标需要做多少工作。in只是做得更少。


18
这是一个难以置信的答案。您与python项目有什么关系?
kdbanman

9
@kdbanman没有,真的,虽然我设法勉强了一点;)。
Veedrac

21
@varepsilon Aww,但是没人理会实际的帖子!问题的重点不是真正的答案,而是获得答案的过程-希望不会有很多人在生产中使用这种黑客手段!
Veedrac'3

181

这里有三个因素在起作用,共同产生这种令人惊讶的行为。

首先:in操作员使用快捷方式并检查身份(x is y),然后再检查是否等于(x == y):

>>> n = float('nan')
>>> n in (n, )
True
>>> n == n
False
>>> n is n
True

第二:由于Python的字符串interning,两个"x"in "x" in ("x", )是相同的:

>>> "x" is "x"
True

(大警告:这是实现特定的行为!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

这种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://stackoverflow.com/a/28889838/71522 -进入每个过程具体是什么情况更多的细节==in,是非常值得读。


3
这样做的原因很可能允许X in [X,Y,Z]在没有XYZ不必定义相等方法的情况下正确工作(或者,默认的相等是is,所以它省去了__eq__在没有用户定义的情况下调用对象的必要__eq__is并且应为true表示值) -平等)。
aruisdante

1
使用float('nan')可能会产生误导。它的一个特性nan是它不等于自身。这可能会改变时间。
dawg,2015年

@dawg啊,好点– nan示例只是为了说明in进行成员资格测试的捷径。我将更改变量名称以进行澄清。
David Wolever'3

3
据我所知,在CPython 3.4.3中tuple.__contains__是通过tuplecontains调用实现的,PyObject_RichCompareBool并且在有身份标识的情况下会立即返回。unicode具有PyUnicode_RichCompare内幕,具有相同的身份快捷方式。
Cristian Ciupitu 2015年

3
这意味着"x" is "x"不一定是这样True'x' in ('x', )会一直如此True,但似乎不会比快==
David Wolever'3
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.