将迭代器传递给any以提高速度,为什么?

4 投票
4 回答
3368 浏览
提问于 2025-04-17 12:20

这里总结了一些问题。是的,我知道其中一些答案 ;) 还有一些我可以简单说说,但我真的想深入探讨一下。

  1. 这真的是个好主意吗?(这个问题在下面)
  2. 我想知道使用 map 真的能提高速度吗?为什么?
  3. 为什么把迭代器传给 any 会让我的代码更快?
  4. 为什么我的 Counter 对象能工作,而我的 print_true 函数却失败得很惨?
  5. 有没有类似于 itertools.imap 的东西,可以反复调用一个函数,或者选择调用一定次数?
  6. 我的胡萝卜在哪里?!

我刚看了 PyCon 2011: Dropbox 是怎么做到的,以及 Python 是如何帮助的(老实说,我大部分时间都在快进),但终于在大约 22:23 的时候开始了真正有趣的内容。

演讲者提到要在 C 语言中写内部循环,并且“只运行一次”的东西不需要太多优化(这很有道理)……然后他接着说……大意是:

把迭代器组合传给 any 可以大幅提高速度。

这里是代码(希望是一样的):

import itertools, hashlib, time   
_md5 = hashlib.md5()  
def run():
    for i in itertools.repeat("foo", 10000000):
        _md5.update(i)
a = time.time();  run(); time.time() - a  
Out[118]: 9.44077205657959

_md5 = hashlib.md5() 
def run():
    any(itertools.imap(_md5.update, itertools.repeat("foo", 10000000)))    
a = time.time();  run(); time.time() - a
Out[121]: 6.547091007232666

嗯,看起来为了更快的速度,我只需要一台更快的电脑! (根据他的幻灯片判断。)

然后他做了一些简单的解释,但并没有详细说明为什么

我之前就知道迭代器的用法,感谢 Alex Martelli 的回答,关于 如何在不使用索引变量的情况下做 N 次事情

然后我想,我想知道 map 真的能提高速度吗?我最后的想法是 WTF??? 把迭代器传给 any?真的???这肯定不对,因为文档中对 any 的定义是:

def any(iterable):
    for element in iterable:
        if element:
            return True
    return False

为什么把迭代器传给 any 会让我的代码更快?

然后我用以下代码进行了测试(还有很多其他测试),但这让我很困惑:

def print_true(x):
    print 'True'
    return 'Awesome'

def test_for_loop_over_iter_map_over_iter_repeat():
    for result in itertools.imap(print_true, itertools.repeat("foo", 5)):
        pass

def run_any_over_iter_map_over_iter_repeat():
    any(itertools.imap(print_true, itertools.repeat("foo", 5)))

And the runs:

    In [67]: test_for_loop_over_iter_map_over_iter_repeat()
    True
    True
    True
    True
    True

    In [74]: run_any_over_iter_map_over_iter_repeat()
    True

真丢人。我心想这个家伙真是胡说八道。异端!!但我冷静下来,继续测试。如果这是真的,Dropbox 怎么可能正常工作呢!?

经过进一步测试,它确实有效……我最开始只是用了一个简单的计数器对象,它在两种情况下都能数到 10000000。

所以问题是,为什么我的 Counter 对象能工作,而我的 print_true 函数却失败得很惨?

class Counter(object):
    count = 0
    def count_one(self, none):
        self.count += 1

def run_any_counter():
    counter = Counter()
    any(itertools.imap(counter.count_one, itertools.repeat("foo", 10000000)))
    print counter.count

def run_for_counter():
    counter = Counter()
    for result in itertools.imap(counter.count_one, itertools.repeat("foo", 10000000)):
        pass
    print counter.count

输出:

%time run_for_counter()
10000000
CPU times: user 5.54 s, sys: 0.03 s, total: 5.57 s
Wall time: 5.68 s

%time run_any_counter()
10000000
CPU times: user 5.28 s, sys: 0.02 s, total: 5.30 s
Wall time: 5.40 s

更让人困惑的是,即使在去掉不必要的参数,并为我的 Counter 对象写了最合理的代码,它仍然比 any-map 版本慢。我的胡萝卜在哪里?!:

class CounterNoArg(object):
    count = 0
    def count_one(self):
        self.count += 1

def straight_count():
    counter = CounterNoArg()
    for _ in itertools.repeat(None, 10000000):
        counter.count_one()
    print counter.count

输出:

In [111]: %time straight_count()
10000000
CPU times: user 5.44 s, sys: 0.02 s, total: 5.46 s
Wall time: 5.60 s

我问这个问题是因为我觉得 Python 爱好者需要一个激励,这样我们就不会开始把东西传给 anyall 来提高性能,或者说已经有类似的东西吗?可能有类似于 itertools.imap 的东西,可以反复调用一个函数,或者选择调用一定次数。

我所能做到的最好是(使用列表推导式会得到有趣的结果):

def super_run():
    counter = CounterNoArg()
    for _ in (call() for call in itertools.repeat(counter.count_one, 10000000)):
        pass
    print counter.count

def super_counter_run():
    counter = CounterNoArg()
    [call() for call in itertools.repeat(counter.count_one, 10000000)]
    print counter.count

def run_any_counter():
    counter = Counter()
    any(itertools.imap(counter.count_one, itertools.repeat("foo", 10000000)))
    print counter.count

%time super_run()
10000000
CPU times: user 5.23 s, sys: 0.03 s, total: 5.26 s
Wall time: 5.43 s

%time super_counter_run()
10000000
CPU times: user 4.75 s, sys: 0.18 s, total: 4.94 s
Wall time: 5.80 s

%time run_any_counter()
10000000
CPU times: user 5.15 s, sys: 0.06 s, total: 5.21 s
Wall time: 5.30 s

def run_any_like_presentation():
    any(itertools.imap(_md5.update, itertools.repeat("foo", 10000000)))

def super_run_like_presentation():
    [do_work for do_work in itertools.imap(_md5.update, itertools.repeat("foo", 10000000))]

def super_run_like_presentation_2():
    [_md5.update(foo) for foo in itertools.repeat("foo", 10000000)]


%time run_any_like_presentation()
CPU times: user 5.28 s, sys: 0.02 s, total: 5.29 s
Wall time: 5.47 s

%time super_run_like_presentation()
CPU times: user 6.14 s, sys: 0.18 s, total: 6.33 s
Wall time: 7.56 s

%time super_run_like_presentation_2()
CPU times: user 8.44 s, sys: 0.22 s, total: 8.66 s
Wall time: 9.59 s

唉……

注意:我鼓励你自己运行这些测试。

4 个回答

1

run_any_counter这个函数没有明确的返回值,所以它返回的是None。在布尔上下文中,None被视为False,因此any会处理整个可迭代对象。

如果你想更通用地处理可迭代对象,可以参考itertools的食谱部分。这个方法不依赖于返回假值。

再说说run_any_like_presentation等函数:imap(f, seq)只查找一次f,而列表推导式[f(x) for x in seq]则对序列中的每个元素都查找一次。

[x for x in imap(f, seq)]其实就是在用另一种方式写list(imap(f, x)),不过两者都会生成一个不必要的列表。

最后,for循环会给循环变量赋值,即使这个变量没有被使用,所以这会稍微慢一点。

2

关于通过使用 any 来优化的问题,答案是否定的。我认为这样做并不是个好主意,主要是因为这不是它的本来用途。虽然实现起来很简单,但维护起来可能会变得非常麻烦。这样做会在你的代码中引入新的问题。如果这个函数返回 false,那么迭代器就不会被完全消耗,这会导致一些奇怪的行为和难以追踪的错误。而且,实际上还有更快(或者至少差不多快)的替代方案可以使用内置的 any。

当然,你可以例外,因为似乎 any 的性能确实比 deque 更好,但使用 any 绝对是极端的做法,而且大多数情况下并不必要。实际上,如果有什么的话,随着 Python 代码库的更新,你可能引入的优化在某些情况下可能不再“最佳”(比如 2.7 和 3.2 之间的差异)。

还有一点需要提到的是,这种使用 any 的方式并不一定是合理的。在使用 any 之前是否需要实现一个 C 扩展也是有争议的。就我个人而言,我更倾向于从语义上考虑这个问题。

至于如何优化自己的代码,我们先看看我们面临的挑战:可以参考 run_any_like_presentation。这个速度相当快 :)

最初的实现可能看起来像这样:

import itertools, hashlib
_md5 = hashlib.md5()
def run():
    for _ in xrange(100000000):
        _md5.update("foo")

第一步是使用 itertools.repeat 来做某件事情 N 次。

def run_just_repeat():
    for foo in itertools.repeat("foo", 100000000):
        _md5.update(foo)

第二个优化是使用 itertools.imap,这样就不需要在 Python 代码中传递 foo 的引用了。现在它是在 C 中处理的。

def run_imap_and_repeat():
    for do_work in itertools.imap(_md5.update, itertools.repeat("foo", 10000000)):
        pass

第三个优化是将整个 for 循环完全移到 C 代码中。

import collections
def run_deque_imap_and_repeat():
    collections.deque(itertools.imap(_md5.update, itertools.repeat("foo", 10000000)))

最后一个优化是将所有可能的查找移到 run 函数的命名空间中:

这个想法来自于 http://docs.python.org/library/itertools.html?highlight=itertools 的最后部分。

注意,上述许多方法可以通过将全局查找替换为定义为默认值的局部变量来优化。

就我个人而言,我在这方面的成功与否参差不齐。也就是说,在某些条件下有小幅提升,从模块导入 xxx 也显示出性能提升,而不需要传递它。此外,有时如果我传入某些变量而不传入其他变量,我也会看到一些微小的差异。关键是,我觉得你需要自己测试一下,看看这对你是否有效。

def run_deque_imap_and_repeat_all_local(deque = collections.deque, 
        imap = itertools.imap, _md5 = _md5, repeat = itertools.repeat, 
        md5 = hashlib.md5):
    update = _md5.update
    deque(imap(_md5.update, repeat("foo", 100000000)), maxlen = 0)

最后,为了公平起见,让我们实现一个类似演示的 any 版本,也进行最终优化。

def run_any_like_presentation_all_local(any = any, deque = collections.deque, 
        imap = itertools.imap, _md5 = _md5, repeat = itertools.repeat, 
        md5 = hashlib.md5):
    any(imap(_md5.update, repeat("foo", 100000000)))

好了,现在让我们运行一些测试(Python 2.7.2 OS X Snow Leopard 64-bit):

  • run_reference - 123.913 秒

  • run_deque_imap_and_repeat_all_local - 51.201 秒

  • run_deque_local_imap_and_repeat - 53.013 秒

  • run_deque_imap_and_repeat - 48.913 秒

  • run_any_like_presentation - 49.833 秒

  • run_any_like_presentation_all_local - 47.780 秒

再来看看 Python3(Python 3.2 OS X Snow Leopard 64-bit):

  • run_reference - 94.273 秒(100000004 次函数调用!)

  • run_deque_imap_and_repeat_all_local - 23.929 秒

  • run_deque_local_imap_and_repeat - 23.298 秒

  • run_deque_imap_and_repeat - 24.201 秒

  • run_any_like_presentation - 24.026 秒

  • run_any_like_presentation_all_local - 25.316 秒

这是我测试的来源:

import itertools, hashlib, collections
_md5 = hashlib.md5()

def run_reference():
    for _ in xrange(100000000):
        _md5.update("foo")

def run_deque_imap_and_repeat_all_local(deque = collections.deque,
        imap = itertools.imap, _md5 = _md5, repeat = itertools.repeat,
        md5 = hashlib.md5):
    deque(imap(_md5.update, repeat("foo", 100000000)), maxlen = 0)

def run_deque_local_imap_and_repeat(deque = collections.deque,
        imap = itertools.imap, _md5 = _md5, repeat = itertools.repeat,
        md5 = hashlib.md5):
    deque(imap(_md5.update, repeat("foo", 100000000)), maxlen = 0)

def run_deque_imap_and_repeat():
    collections.deque(itertools.imap(_md5.update, itertools.repeat("foo", 100000000)),
            maxlen = 0)

def run_any_like_presentation():
    any(itertools.imap(_md5.update, itertools.repeat("foo", 100000000)))

def run_any_like_presentation_all_local(any = any, deque = collections.deque,
        imap = itertools.imap, _md5 = _md5, repeat = itertools.repeat,
        md5 = hashlib.md5):
    any(imap(_md5.update, repeat("foo", 100000000)))

import cProfile
import pstats

def performance_test(a_func):
    cProfile.run(a_func, 'stats')
    p = pstats.Stats('stats')
    p.sort_stats('time').print_stats(10)

performance_test('run_reference()')
performance_test('run_deque_imap_and_repeat_all_local()')
performance_test('run_deque_local_imap_and_repeat()')
performance_test('run_deque_imap_and_repeat()')
performance_test('run_any_like_presentation()')
performance_test('run_any_like_presentation_all_local()')

还有 Python3

import itertools, hashlib, collections
_md5 = hashlib.md5()

def run_reference(foo = "foo".encode('utf-8')):
    for _ in range(100000000):
        _md5.update(foo)

def run_deque_imap_and_repeat_all_local(deque = collections.deque,
        imap = map, _md5 = _md5, repeat = itertools.repeat,
        md5 = hashlib.md5):
    deque(imap(_md5.update, repeat("foo".encode('utf-8'), 100000000)), maxlen = 0)

def run_deque_local_imap_and_repeat(deque = collections.deque,
        imap = map, _md5 = _md5, repeat = itertools.repeat,
        md5 = hashlib.md5):
    deque(imap(_md5.update, repeat("foo".encode('utf-8'), 100000000)), maxlen = 0)

def run_deque_imap_and_repeat():
    collections.deque(map(_md5.update, itertools.repeat("foo".encode('utf-8'), 100000000)),
            maxlen = 0)

def run_any_like_presentation():
    any(map(_md5.update, itertools.repeat("foo".encode('utf-8'), 100000000)))

def run_any_like_presentation_all_local(any = any, deque = collections.deque,
        imap = map, _md5 = _md5, repeat = itertools.repeat):
    any(imap(_md5.update, repeat("foo".encode('utf-8'), 100000000)))

import cProfile
import pstats

def performance_test(a_func):
    cProfile.run(a_func, 'stats')
    p = pstats.Stats('stats')
    p.sort_stats('time').print_stats(10)

performance_test('run_reference()')
performance_test('run_deque_imap_and_repeat_all_local()')
performance_test('run_deque_local_imap_and_repeat()')
performance_test('run_deque_imap_and_repeat()')
performance_test('run_any_like_presentation()')
performance_test('run_any_like_presentation_all_local()')

还有一点,除非确实存在可验证的性能瓶颈,否则不要在真实项目中这样做。

最后,如果在某些情况下 any 确实比 deque 更快,我们的代码会在新版本的 Python 中更容易利用改进,而不需要修改你的代码库,这样的代码更合理。

http://www.python.org/doc/essays/list2str/ 是关于如何进行 Python 优化的好文章。(也就是说,理想情况下,编写 C 扩展并不是你首先想到的事情)。

我还想指出 Gareth 的回答,因为他可能找到了 any 比 deque 更快的原因。

4

在你的第一个例子中,第一种版本的 run 每次循环都要查找 _md5.update,而第二种版本就不需要这样做。我觉得这就是大部分性能差异的原因。剩下的可能是因为需要设置局部变量 i,不过这点不太好演示。

import itertools, hashlib, timeit
_md5 = hashlib.md5()

def run1():
    for i in itertools.repeat("foo", 10000000):
        _md5.update(i)

def run2():
    u = _md5.update
    for i in itertools.repeat("foo", 10000000):
        u(i)

def run3():
    any(itertools.imap(_md5.update, itertools.repeat("foo", 10000000)))

>>> timeit.timeit('run1()', 'from __main__ import run1', number=1)
6.081272840499878
>>> timeit.timeit('run2()', 'from __main__ import run2', number=1)
4.660238981246948
>>> timeit.timeit('run3()', 'from __main__ import run3', number=1)
4.062871932983398

itertools 的文档里有一个更好的方法来处理迭代器(并丢弃它的所有值):可以看看 consume 函数。使用 any 来完成这个任务是因为 _md5.update 总是返回 None,所以这种方法并不适用于所有情况。另外,这个方法稍微快一点: [见评论]

import collections

def consume(it):
    "Consume iterator completely (discarding its values)."
    collections.deque(it, maxlen=0)

def run4():
    consume(itertools.imap(_md5.update, itertools.repeat("foo", 10000000)))

>>> timeit.timeit('run4()', 'from __main__ import run4', number=1)
3.969902992248535

补充说明:似乎 consume 的方法没有得到应有的关注:如果你查看 CPython 的实现细节,会发现当调用 collections.deque 并设置 maxlen=0 时,它会调用一个函数 consume_iterator_collectionsmodule.c 中,代码大概是这样的:

static PyObject*
consume_iterator(PyObject *it)
{
    PyObject *item;
    while ((item = PyIter_Next(it)) != NULL) {
        Py_DECREF(item);
    }
    Py_DECREF(it);
    if (PyErr_Occurred())
        return NULL;
    Py_RETURN_NONE;
}

撰写回答