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

问题描述 投票:5回答:4

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

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

我只是看了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

啊...

注意:我鼓励您自己运行测试。

python performance
4个回答
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
投票

run_any_counter函数没有明确的返回值,因此返回None,它在布尔上下文中是False,因此any消耗整个iterable。

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循环分配给循环变量,即使它没有被使用。所以这稍微慢一些。


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。


0
投票

然后他做了一堆手挥手而没有真正详细说明为什么。

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

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

any在找到真正的返回值后立即停止,因为它知道满足“任何”条件(短路评估)。

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

© www.soinside.com 2019 - 2024. All rights reserved.