将迭代器传递给任何执行速度和为什么?

时间:2012-02-04 21:58:03

标签: python performance

这里总结了问题。是的,我知道一些这些答案;)我可以在其他人身上挥手,但我真的很喜欢这里的细节。

  1. 这是一个好主意吗? (这个在下面)
  2. 我想知道地图是否真的能提高速度?为什么?
  3. 为什么世界会将迭代器传递给any使我的代码更快?
  4. 为什么我的Counter对象工作,我的print_true函数失败了?
  5. 是否有一个等同于itertools.imap的函数会一遍又一遍地调用函数,并且可选择一定次数?
  6. 我的胡萝卜在哪里?!?

  7. 我刚看了PyCon 2011: How Dropbox Did It and How Python Helped(不可否认我跳过了大部分内容),但最后真正有趣的事情始于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,我已经从pythonic way to do something N times without an index variable?的答案中了解了迭代器。

    然后我想,我想知道地图是否真的能提高速度?我最后的想法是WTF ???转到any?真???当然,这可能是正确的,因为文档将any定义为:

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

    为什么世界上会将迭代器传递给任何使代码更快的代码?

    然后,我使用以下(在许多其他测试中)对其进行了测试,但这就是让我:

    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
    

    羞耻。我宣布这个GUY是完全的。的异端!但是,我平静下来并继续测试。 如果这是真的,那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
    

    甚至更大的WTF甚至在删除不需要的参数并为我的Counter对象编写最合理的代码之后,它仍然比任何地图版本慢。我的胡萝卜在哪里?!?:

    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
    

    我问,因为我认为Pythonistas或Pythoneers需要胡萝卜,所以我们不会开始将内容传递给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 个答案:

答案 0 :(得分: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 documentation有更好的消耗迭代器的方法(并丢弃其所有值):请参阅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 in _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;
}

答案 1 :(得分:1)

run_any_counter函数没有显式返回值,因此返回None,它在布尔上下文中为False,因此any使用整个可迭代。

recipes section for itertools中给出了使用迭代的更一般方法。它并不依赖于错误的返回值。

比较run_any_like_presentation等: imap(f, seq)仅查找f一次,而列表推导[f(x) for x in seq]为seq的每个元素执行此操作。 [x for x in imap(f, seq)]是一种有趣的拼写list(imap(f, x))方式,但都会构建一个不必要的列表。

最后,for循环分配给循环变量,即使它没有被使用。所以这稍微慢一些。

答案 2 :(得分:1)

通过传递给任何人来回答关于优化的第一个问题。不,我认为这不是一个好主意,因为这不是它的预期目的。当然,它很容易实现,但维护可能会成为一场噩梦。通过这样做,您的代码库中引入了新的问题。如果函数返回false,那么迭代器将不会被完全消耗,导致奇怪的行为,以及难以追踪的错误。此外,存在更快(或至少几乎同样快)的替代方案来使用内置的任何。

当然,你可以做一个例外,因为看起来任何人都可以真正执行deque,但使用any肯定是极端的,而且通常是不必要的。事实上,如果有的话,你可能会引入优化,在更新Python代码库之后它们可能不再是“最优的”(见2.7 vs 3.2)。

另一件值得一提的是,任何使用都不会立即产生任何意义。在使用任何类似的东西之前是否实现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)

最后,为了公平,让我们实现任何版本,例如进行最终优化的演示文稿。

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位):

  • 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位)中的踢法:

  • 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()')

另一方面,除非存在可认证的性能瓶颈,否则不要在真实项目中执行此操作。

最后,如果我们真的需要一个胡萝卜(除了编写有意义且不容易出错的代码),在任何实际执行deque的困难时期,你的更合理的代码将更好在不必修改代码库的情况下利用新版Python的改进。

http://www.python.org/doc/essays/list2str/是关于如何进行Python优化的好读物。 (即理想情况下写一个C扩展并不是你要达到的第一件事。)

我还想指出Gareth的回答,因为他可能会解释为什么任何人都能表演deque。

答案 3 :(得分:0)

  然后他做了一堆手挥手而没有详细说明原因。

因为实际的循环是本机完成的,而不是通过解释Python字节码。

  

为什么我的Counter对象工作且我的print_true函数失败了?

any一旦找到true-ish返回值就会停止,因为它知道满足“any”条件(短路评估)。

print_true返回"awesome",这是真的。 counter.count_one没有明确的return,因此返回None,这是假的。