为什么使用“any()”比使用循环运行速度更慢?

12

我一直在做一个管理大量单词列表并通过许多测试来验证列表中每个单词的项目。有趣的是,每次我使用像 itertools 模块这样“更快”的工具时,它们似乎反而更慢。

最终我决定提出这个问题,因为我可能做错了什么。以下代码将尝试测试 any() 函数与循环使用的性能差异。

#!/usr/bin/python3
#

import time
from unicodedata import normalize


file_path='./tests'


start=time.time()
with open(file_path, encoding='utf-8', mode='rt') as f:
    tests_list=f.read()
print('File reading done in {} seconds'.format(time.time() - start))

start=time.time()
tests_list=[line.strip() for line in normalize('NFC',tests_list).splitlines()]
print('String formalization, and list strip done in {} seconds'.format(time.time()-start))
print('{} strings'.format(len(tests_list)))


unallowed_combinations=['ab','ac','ad','ae','af','ag','ah','ai','af','ax',
                        'ae','rt','rz','bt','du','iz','ip','uy','io','ik',
                        'il','iw','ww','wp']


def combination_is_valid(string):
    if any(combination in string for combination in unallowed_combinations):
        return False

    return True


def combination_is_valid2(string):
    for combination in unallowed_combinations:
        if combination in string:
            return False

    return True


print('Testing the performance of any()')

start=time.time()
for string in tests_list:
    combination_is_valid(string)
print('combination_is_valid ended in {} seconds'.format(time.time()-start))


start=time.time()
for string in tests_list:
    combination_is_valid2(string)
print('combination_is_valid2 ended in {} seconds'.format(time.time()-start))  

前面的代码很好地代表了我所做的测试类型,如果我们看一下结果:

File reading done in 0.22988605499267578 seconds
String formalization, and list strip done in 6.803032875061035 seconds
38709922 strings
Testing the performance of any()
combination_is_valid ended in 80.74802565574646 seconds
combination_is_valid2 ended in 41.69514226913452 seconds


File reading done in 0.24268722534179688 seconds
String formalization, and list strip done in 6.720442771911621 seconds
38709922 strings
Testing the performance of any()
combination_is_valid ended in 79.05265760421753 seconds
combination_is_valid2 ended in 42.24800777435303 seconds

我发现使用循环比使用any()快了一半,这真的很惊人。有什么解释吗?是我做错了什么吗?

(我在GNU-Linux下使用Python3.4)


你的测试向量中是否包含任何字符串会返回 True - Ignacio Vazquez-Abrams
1
可能是因为生成器表达式在循环上提供了一定的间接性,从而导致速度变慢。 - interjay
关于你所说的循环提前退出:any 也会提前退出(只迭代到第一个真值),所以这不是区别。 - interjay
2个回答

4
实际上,any() 函数等同于以下函数:
def any(iterable):
    for element in iterable:
        if element:
            return True
    return False

这段代码类似于你的第二个函数,但由于any()本身返回一个布尔值,所以你不需要检查结果然后返回一个新值,因此性能差异在于你实际上使用了冗余的返回和if条件,并在另一个函数中调用了any
因此,any的优点在于你不需要将其包装在另一个函数中,因为它已经为你完成了所有操作。
正如@interjay在评论中提到的那样,最重要的原因是你将生成器表达式传递给any(),它不会立即提供结果,而是按需产生结果,因此它会执行额外的工作。
基于PEP 0289 -- Generator Expressions的语义,生成器表达式的语义等效于创建一个匿名生成器函数并调用它。
g = (x**2 for x in range(10))
print g.next()

等同于:

def __gen(exp):
    for x in exp:
        yield x**2
g = __gen(iter(range(10)))
print g.next()

因此,正如您所看到的,每次Python要访问下一个项目时,它都会调用生成器的iter函数和next方法。最终的结果是,在这种情况下使用any()是过度的。


5
与循环相比,增加一个if条件语句对性能的影响微乎其微。更大的区别在于any版本使用了生成器表达式。 - interjay
3
这个循环在寻找多个子字符串时会执行很多工作。增加一个函数调用和一个“if”语句几乎不可能使时间翻倍。 - interjay
@interjay 是的,我明白了。感谢您的关注,我刚刚根据您的提示更新了答案。 - Mazdak
通过使用生成器表达式,any 每个元素都需要使用一个额外的函数调用。所以我认为 @interjay 在这里是正确的。 - Tali
转换为列表推导式可能会节省一些开销,但也会阻止“any”评估短路。这可能不是净积极的。如果速度差异很重要,则显式循环将是最快的。 - user2357112
显示剩余2条评论

1

既然您的真正问题已经得到解答,我会尝试回答隐含的问题:

您可以通过执行unallowed_combinations = sorted(set(unallowed_combinations))来获得免费的速度提升,因为它包含重复项。

鉴于此,我所知道的最快方法是:

valid3_re = re.compile("|".join(map(re.escape, unallowed_combinations)))

def combination_is_valid3(string):
    return not valid3_re.search(string)

使用CPython 3.5,对于一些行长为60个字符的测试数据,我得到了以下结果:

combination_is_valid ended in 3.3051061630249023 seconds
combination_is_valid2 ended in 2.216959238052368 seconds
combination_is_valid3 ended in 1.4767844676971436 seconds

其中第三个是正则表达式版本,在PyPy3上我得到了

combination_is_valid ended in 2.2926249504089355 seconds
combination_is_valid2 ended in 2.0935239791870117 seconds
combination_is_valid3 ended in 0.14300894737243652 seconds

FWIW,这与Rust(一种类似于C ++的低级语言)相竞争,并且在正则表达式方面实际上明显胜出。较短的字符串更倾向于PyPy而不是CPython(例如,对于行长度为10,CPython的4倍),因为开销比重要。

由于CPython的正则表达式运行时间仅有三分之一是循环开销,我们得出结论:PyPy的正则表达式实现针对此用例进行了更好的优化。我建议查看是否存在CPython正则表达式实现,使其能够与PyPy竞争。


在输入测试时,我将重复的值放入了不允许组合列表中,这是我的错误。非常感谢您的回答!我的基准测试结果为... 22.354313850402832 秒 - user3672754

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接