2017-06-28 27 views
4

我的any()内置功能的性能与实际执行比较docs建议:的Python:任何()意外性能

,我在下面的列表中寻找大于0的元素:

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 

我希望两者的拥有完全相同的性能,但是如果any()两次慢。为什么?

+0

您是否尝试过使用不是以'0'开头的异构'lst'? –

+0

一个更加等效的版本将是:'%timeit any(对于elm,如果elm> 0,则为真)'。 –

+0

还有'any()'是在Python中的实际实现还是只是*等价的* Python语法? –

回答

4

原因是您已将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的答案。

+0

in'gt_0'尽管 –

+1

我仍然认为你的数据是误导性的,但OP仍然在函数中进行比较。你不能只比较'%timeit any(elm> 0 for elm in lst)'和'%timeit any(test_list)',你还需要考虑构建'test_list'所花费的时间。这些是我的结果:'%timeit test_list = [elm> 0 for elm in lst];任何(test_list);'每个循环输出52.5 ms,而'%timeit any(elm> 0 for elm in lst)'报告每循环38.4 ms。所以发生器表达式实际上更好。 – dabadaba

+0

@dabadaba这不是我要做的。当然,创建列表并将它传递给任何一个都比将生成器表达式传递给它要慢。重点是你的时间之间的差异。您正在将列表传递给您的手动函数,而您正在使用生成器表达式的“任何”。 – Kasramvd

1

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

在您的any中,有两个for循环:for elm in lst和for循环由any执行。因此,任何迭代看起来像False, False, False, ..., True

False, False, False, ..., True在您的gt_0,只有一个for循环。

如果你改变它来检查,如果该元素是truthy可言,所以他们都只有一次循环:

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 
+1

我没有看到你说的有两个'for'循环。 – dabadaba

1
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 

正如Kasramvd所解释的,最后一个版本是最慢的版本,因为它使用了生成器表达式;列表理解有点快,但是 - 令人惊讶的是 - 使用Ashwini Chaudhary提出的带条件子句的生成器表达式更快。

+0

我没有得到这些结果。我得到17.4毫秒,39.1毫秒和52.4毫秒。有意义的是,列表理解是最慢的,因为它需要建立一个全新的列表,而生成器表达式更快,并且当条件满足时也停止。在这里,最后一件事情没有太大的影响,因为我们知道条件直到最后一个元素才会被满足,但要留意是否将'1'移动到列表的开头:每个循环47毫秒,列表理解430纳秒用发生器表达式。是的,纳秒。巨大的差异。 – dabadaba

3

当然,生成器表达式的循环比列表慢。但是在这种情况下,生成器内的迭代基本上是列表本身的循环,因此调用生成器的基本上委托给列表的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检查一次条件的真值,并基于此返回TrueFalse

现在让我们添加另一个基于any()的版本,该版本在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 

现在,我们已经通过添加条件(查询详细最后一节)减少any()所做的工作,它会在条件检查truthy两次只有一次将会是True,否则它将基本上跳到下一个项目。


现在让我们来比较这三个时序:

>>> %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_0包括两个检查是在简单any()版本,并检查它的时机。

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的truthy价值会发生什么。

>> %%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)已经布尔对象上,我想那种解释的基本any()版本缓慢。


你可能会说,if条件any()也将导致两个感实性检查,但情况并非如此,当comparison operation回报Py_TruePy_FalsePOP_JUMP_IF_FALSE只是跳到下一个OP代码,并且没有对PyObject_IsTrue进行调用。

+0

我不认为在这里是否有额外的比较,因为我们可以看到的是,我们都在进行一个比较,在任何一个比较中,都是常规程序。这似乎是因为python评估这种比较的方式(在python中和/或将其委托给内置函数)。当我们将额外的'condition = elm> 0'添加到手动函数时,它将在Python层中执行,而不像编译器表达式那样在编译的代码对象中执行。我认为当我们将'elm> 0'传递给'any'而不是bool对象时会发生这种情况。 – Kasramvd

+0

@Kasramvd我没有说额外的比较,只是'elm> 0'首先被转换为一个布尔值,然后'any()'每次都再次检查它的真实性。 –

+0

对不起,这是我解释你的答案的方式。因为在第二种情况下,如果我们在生成器表达式中进行比较,则仍然会创建布尔值并再次检查真实性。我认为我们在第二种情况的字节码中看到一个额外的'POP_JUMP_IF_FALSE'的原因是因为'any'遇到了bool对象而不是比较,并且在这两种情况下操作次数都是相同的,似乎区别在于python评估该比较的方式。 – Kasramvd