这里总结了问题。是的,我知道一些这些答案;)我可以在其他人身上挥手,但我真的很喜欢这里的细节。
我刚看了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需要胡萝卜,所以我们不会开始将内容传递给any或all以提高性能,还是已经存在?可能相当于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
啊...
注意:我鼓励您自己运行测试。
答案 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
,这是假的。