这里总结了一些问题。是的,我知道其中一些答案;)我可以在其他人身上挥手,但我真的很喜欢这里的细节。
我只是看了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
啊...
注意:我鼓励您自己运行测试。
在你的第一个例子中,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;
}
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循环分配给循环变量,即使它没有被使用。所以这稍微慢一些。
通过传递给任何人来回答关于优化的第一个问题。不,我认为这不是一个好主意,因为这不是它的预期目的。当然,它很容易实现,但维护可能会成为一场噩梦。通过这样做,您的代码库中引入了新的问题。如果函数返回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位):
而且只是用于Python3(Python 3.2 OS X Snow Leopard 64位)中的踢法:
这是测试的来源:
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。
然后他做了一堆手挥手而没有真正详细说明为什么。
因为实际的循环是本机完成的,而不是通过解释Python字节码。
为什么我的Counter对象工作,我的print_true函数失败了?
any
在找到真正的返回值后立即停止,因为它知道满足“任何”条件(短路评估)。
print_true
返回"awesome"
,这是真的。 counter.count_one
没有明确的return
,所以它返回None
,这是假的。