Python timeit 的惊人结果:Counter() vs defaultdict() vs dict()

19

我用 timeit 得到了非常惊人的结果,请问我是否做错了什么?我正在使用 Python 2.7。

这是文件 speedtest_init.py 的内容:

import random

to_count = [random.randint(0, 100) for r in range(60)]

这是speedtest.py的内容:

__author__ = 'BlueTrin'

import timeit

def test_init1():
    print(timeit.timeit('import speedtest_init'))

def test_counter1():
    s = """\
    d = defaultdict(int);
    for i in speedtest_init.to_count:
        d[i] += 1
    """
    print(timeit.timeit(s, 'from collections import defaultdict; import speedtest_init;'))

def test_counter2():
    print(timeit.timeit('d = Counter(speedtest_init.to_count);', 'from collections import Counter; import speedtest_init;'))


if __name__ == "__main__":
    test_init1()
    test_counter1()
    test_counter2()

控制台输出为:

C:\Python27\python.exe C:/Dev/codility/chlorum2014/speedtest.py
2.71501962931
65.7090444503
91.2953839048

Process finished with exit code 0

我认为默认情况下,timeit() 会运行代码1000000次,因此我需要将时间除以1000000,但令人惊讶的是Counter比defaultdict()更慢。

这是否符合预期?

编辑:

使用dict比defaultdict(int)更快:

def test_counter3():
    s = """\
    d = {};
    for i in speedtest_init.to_count:
        if i not in d:
            d[i] = 1
        else:
            d[i] += 1
    """
    print(timeit.timeit(stmt=s, setup='from collections import defaultdict; import speedtest_init;')

这个最新版本比defaultdict(int)更快,这意味着除非你更关心可读性,否则你应该使用dict()而不是defaultdict()。

1个回答

25

是的,这是预期的;Counter()构造器 使用 Counter.update(),该方法使用 self.get() 加载初始值,而不是依赖于 __missing__

此外,默认字典 defaultdict__missing__ 工厂完全由 C 代码处理,特别是当使用另一种类型(如实现在 C 中的 int())时。 Counter 源代码是纯 Python,因此 Counter.__missing__ 方法需要一个 Python 帧来执行。

因为 dict.get() 仍然由 C 处理,所以构造器方法是使用 Counter() 的更快方法,只要您使用与 Counter.update() 相同的技巧,并将 self.get 查找别名为本地变量:

>>> import timeit
>>> import random
>>> import sys
>>> sys.version_info
sys.version_info(major=2, minor=7, micro=9, releaselevel='final', serial=0)
>>> to_count = [random.randint(0, 100) for r in range(60)]
>>> timeit.timeit('for i in to_count: c[i] += 1',
...               'from collections import Counter; from __main__ import to_count; c = Counter()',
...               number=10000)
0.2510359287261963
>>> timeit.timeit('for i in to_count: c[i] = c_get(i, 0) + 1',
...               'from collections import Counter; from __main__ import to_count; c = Counter(); c_get = c.get',
...               number=10000)
0.20978617668151855

defaultdictCounter都是为了它们的功能而建立的有用类,而不是为了性能;如果不依赖于__missing__钩子,可能会更快:

>>> timeit.timeit('for i in to_count: d[i] = d_get(i, 0) + 1',
...               'from __main__ import to_count; d = {}; d_get = d.get',
...               number=10000)
0.11437392234802246

这是一个使用别名 dict.get() 方法以达到最大速度的普通字典。但是您还需要重新实现Counter的包行为,或者是Counter.most_common()方法。defaultdict的用例远不止计数。

在Python 3.2中,通过添加处理此情况的C库,更新 Counter() 的速度得到了提升;请参见问题10667。在Python 3.4上进行测试,Counter()构造函数现在已经超过了使用别名 dict.get() 方法的情况:

>>> timeit.timeit('Counter(to_count)',
...               'from collections import Counter; from __main__ import to_count',
...               number=100000)
0.8332311600097455
>>> timeit.timeit('for i in to_count: d[i] = d_get(i, 0) + 1',
...               'from __main__ import to_count; d = {}; d_get = d.get',
...               number=100000)
0.961191965994658
>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=4, micro=2, releaselevel='final', serial=0)

(注:为了得到有意义的计时结果,迭代次数从10k增加到100k;因此,如果您将这些结果与上面的dict.get()案例进行比较,则需要将那里的时间乘以十,即1.144秒)。


5
截至2020年,使用Python 3.7或更高版本时,情况已经改变了——Counter更快。 - bones.felipe
2
@bones.felipe 请阅读整个答案。问题是关于Python 2.7的,但我提到从Python 3.2开始Counter更快,因此从2012年(8年前)开始。 - Martijn Pieters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接