将迭代器传递给 any 以执行速度和原因



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

  1. 这到底是个好主意吗?(这个不在下面(
  2. 我想知道地图是否真的增加了速度改进?为什么?
  3. 为什么将迭代器传递给任何人会使我的代码更快?
  4. 为什么我的计数器对象工作而我的print_true函数惨遭失败?
  5. 是否有一个等效的 itertools.imap 可以一遍又一遍地调用一个函数,并且可以选择一定次数?
  6. 我的胡萝卜在哪里?!?

我刚刚看了 PyCon 2011:Dropbox 如何做到和 Python 如何帮助(诚然,我跳过了大部分部分(,但最终真正有趣的东西在 22:23 左右开始。

演讲者主张用 C 语言制作内部循环,并且"运行一次"的东西不需要太多优化(有意义(......然后他继续陈述...释义:

将迭代器的组合传递给任何迭代器,以提高速度。

这是代码(希望它是相同的(:

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

嗯,看起来为了提高速度,我可以得到一台更快的计算机!(从他的幻灯片来看。

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

我已经从 pythonic 方式的答案中知道了迭代器,可以在没有索引变量的情况下做 N 次的事情? 感谢亚历克斯·马泰利。

然后我想,我想知道地图是否真的增加了速度改进?我最后的想法是WTF???传给任何人?真???当然,这不可能是正确的,因为文档将任何定义为:

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

为了羞耻。 我宣布这家伙充满了它。 异端! 但是,我冷静下来,继续测试。 如果这是真的,Dropbox到底怎么能工作呢?!?

通过进一步的测试,它确实有效...我最初只使用一个简单的计数器对象,在这两种情况下,它都一直计数到 10000000。

所以问题是为什么我的计数器对象工作而我的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 需要一个胡萝卜,这样我们就不会开始将东西传递给任何或所有内容以提高性能,或者已经存在吗?可能等效于 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

呸。。。

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

在第一个示例中,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 实现的细节,你会发现当用 maxlen=0 调用 collections.deque 时,它会在 _collectionsmodule.c 中调用函数consume_iterator,看起来像这样:

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;
}

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

的任何东西。

当然,你可以破例,因为似乎任何一种实际上都可以执行deque,但使用任何肯定是极端的,而且通常是不必要的。事实上,如果有的话,你可能会引入优化,在更新 Python 代码库后,它们可能不再是"最佳"的(参见 2.7 与 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。

run_any_counter函数没有显式返回值,因此返回 None ,这是在布尔上下文中False的,因此any消耗整个可迭代对象。

使用迭代对象的更通用方法在迭代工具的配方部分中给出。它不依赖于错误的返回值。

比较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 循环分配给循环变量,即使它未被使用。所以速度稍微慢一些。

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

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

为什么我的计数器对象工作而我的print_true函数惨遭失败?

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

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

最新更新