代码之家  ›  专栏  ›  技术社区  ›  dabadaba

Python:any()意外性能

  •  10
  • dabadaba  · 技术社区  · 9 年前

    any() 具有实际实现的内置函数 docs

    lst = [0 for _ in range(1000000)] + [1]
    

    这是假定的等效函数:

    def gt_0(lst):
        for elm in lst:
            if elm > 0:
                return True
        return False
    

    以下是性能测试的结果:

    >> %timeit any(elm > 0 for elm in lst)
    >> 10 loops, best of 3: 35.9 ms per loop
    
    >> %timeit gt_0(lst)
    >> 100 loops, best of 3: 16 ms per loop
    

    然而,我希望两者的性能完全相同 任何() 如果慢两倍。为什么?

    4 回复  |  直到 9 年前
        1
  •  11
  •   Mazdak    9 年前

    原因是你通过了 generator expression any() 作用Python需要将生成器表达式转换为生成器函数,这就是它执行速度较慢的原因。因为生成器函数需要调用 __next__() 方法每次生成项并将其传递给 any 。这是指在手动定义的函数中,您将整个列表传递给已准备好所有项的函数。

    使用列表理解而不是生成器表达式可以更好地看到差异:

    In [4]: %timeit any(elm > 0 for elm in lst)
    10 loops, best of 3: 66.8 ms per loop
    
    In [6]: test_list = [elm > 0 for elm in lst]
    
    In [7]: %timeit any(test_list)
    100 loops, best of 3: 4.93 ms per loop
    

    next

    any(True for elm in lst if elm > 0)
    

    在本例中,您正在与生成器表达式进行比较,它将与手动定义的函数在几乎相同的时间内执行(我想,最微小的差异是因为生成器的缘故) Ashwini 的答案。

        2
  •  7
  •   Ashwini Chaudhary    9 年前

    当然,与列表相比,生成器表达式上的循环速度较慢。但在这种情况下,生成器内的迭代基本上是列表本身上的循环,因此 next() 下一个()

    例如,在这种情况下,没有2倍的性能差异。

    >>> lst = list(range(10**5))
    
    >>> %%timeit
    ... sum(x for x in lst)
    ...
    100 loops, best of 3: 6.39 ms per loop
    
    >>> %%timeit
    ... c = 0
    ... for x in lst: c += x
    ...
    
    100 loops, best of 3: 6.69 ms per loop
    

    def gt_0(lst):
        for elm in lst:
            if elm > 0:
                return True
        return False
    
    
    def any_with_ge(lst):
        return any(elm > 0 for elm in lst)
    

    >>> dis.dis(gt_0)
     10           0 SETUP_LOOP              30 (to 33)
                  3 LOAD_FAST                0 (lst)
                  6 GET_ITER
            >>    7 FOR_ITER                22 (to 32)
                 10 STORE_FAST               1 (elm)
    
     11          13 LOAD_FAST                1 (elm)
                 16 LOAD_CONST               1 (0)
                 19 COMPARE_OP               4 (>)
                 22 POP_JUMP_IF_FALSE        7
    
     12          25 LOAD_GLOBAL              0 (True)
                 28 RETURN_VALUE
                 29 JUMP_ABSOLUTE            7
            >>   32 POP_BLOCK
    
     13     >>   33 LOAD_GLOBAL              1 (False)
                 36 RETURN_VALUE
    >>> dis.dis(any_with_ge.func_code.co_consts[1])
     17           0 LOAD_FAST                0 (.0)
            >>    3 FOR_ITER                17 (to 23)
                  6 STORE_FAST               1 (elm)
                  9 LOAD_FAST                1 (elm)
                 12 LOAD_CONST               0 (0)
                 15 COMPARE_OP               4 (>)
                 18 YIELD_VALUE
                 19 POP_TOP
                 20 JUMP_ABSOLUTE            3
            >>   23 LOAD_CONST               1 (None)
                 26 RETURN_VALUE
    

    正如你所看到的,在 any() 版本,它基本上得到了 > 比较,然后使用 PyObject_IsTrue gt_0 检查条件的真实值一次并返回 True False 基于此。

    基于的版本,具有类似for循环中的if条件。

    def any_with_ge_and_condition(lst):
        return any(True for elm in lst if elm > 0)
    

    字节码:

    >>> dis.dis(any_with_ge_and_condition.func_code.co_consts[1])
     21           0 LOAD_FAST                0 (.0)
            >>    3 FOR_ITER                23 (to 29)
                  6 STORE_FAST               1 (elm)
                  9 LOAD_FAST                1 (elm)
                 12 LOAD_CONST               0 (0)
                 15 COMPARE_OP               4 (>)
                 18 POP_JUMP_IF_FALSE        3
                 21 LOAD_GLOBAL              0 (True)
                 24 YIELD_VALUE
                 25 POP_TOP
                 26 JUMP_ABSOLUTE            3
            >>   29 LOAD_CONST               1 (None)
                 32 RETURN_VALUE
    

    现在我们减少了 任何() 通过添加条件(查看最后一节了解更多详细信息),当条件将被更改时,它只需检查truthy两次 真的 ,否则基本上会跳到下一项。


    >>> %timeit gt_0(lst)
    10 loops, best of 3: 26.1 ms per loop
    >>> %timeit any_with_ge(lst)
    10 loops, best of 3: 57.7 ms per loop
    >>> %timeit any_with_ge_and_condition(lst)
    10 loops, best of 3: 26.8 ms per loop
    

    让我们修改 gt\u 0 任何() 版本并检查其计时。

    from operator import truth
    # This calls `PyObject_IsTrue` internally
    # https://github.com/python/cpython/blob/master/Modules/_operator.c#L30
    
    
    def gt_0_truth(lst, truth=truth): # truth=truth to prevent global lookups
        for elm in lst:
            condition = elm > 0
            if truth(condition):
                return True
        return False
    

    时间安排:

    >>> %timeit gt_0_truth(lst)
    10 loops, best of 3: 56.6 ms per loop
    

    operator.truth .

    >> %%timeit t=truth
    ... [t(i) for i in xrange(10**5)]
    ...
    100 loops, best of 3: 5.45 ms per loop
    >>> %%timeit t=truth
    [t(t(i)) for i in xrange(10**5)]
    ...
    100 loops, best of 3: 9.06 ms per loop
    >>> %%timeit t=truth
    [t(i) for i in xrange(10**6)]
    ...
    10 loops, best of 3: 58.8 ms per loop
    >>> %%timeit t=truth
    [t(t(i)) for i in xrange(10**6)]
    ...
    10 loops, best of 3: 87.8 ms per loop
    

    truth() (即 PyObject_IsTrue )在一个已经是布尔型的对象上,我想这可以解释基本 任何()


    if 中的条件 任何() comparison operation Py_True Py_False POP_JUMP_IF_FALSE 只需跳转到下一个操作代码,无需调用 PyObject_IsTrue 是制造的。

        3
  •  1
  •   Artyer    9 年前

    性能的主要部分归结为 for 循环。

    在你的 any for elm in lst 以及由执行的for循环 任何 。因此,任何对生成器的迭代 False, False, False, ..., True

    gt_0 ,只有一个for循环。

    如果将其更改为检查元素是否真实,那么它们都只循环一次:

    def _any(lst):
        for elm in lst:
            if elm:
                return True
        return False
    
    _any(lst)
    
    any(lst)
    

    有一个明显的赢家:

    $ python2 -m timeit "from test import lst, _any" "any(lst)"
    100 loops, best of 3: 5.68 msec per loop
    
    $ python2 -m timeit "from test import lst, _any" "_any(lst)"
    10 loops, best of 3: 17 msec per loop
    
        4
  •  1
  •   Błotosmętek    9 年前
    print(timeit('any(True for elm in lst if elm > 0)',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
    print(timeit('any([elm > 0 for elm in lst])',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
    print(timeit('any(elm > 0 for elm in lst)',setup='lst = [0 for _ in range(1000000)] + [1]', number=10))
    

    2.1382904349993623
    3.1172365920028824
    4.580027656000311