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

Blu*_*rin 14 python counter timeit python-2.7 defaultdict

我用timeit获得了非常令人惊讶的结果,有人可以告诉我,如果我做错了吗?我使用的是Python 2.7.

这是文件speedtest_init.py的内容:

import random

to_count = [random.randint(0, 100) for r in range(60)]
Run Code Online (Sandbox Code Playgroud)

这些是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()
Run Code Online (Sandbox Code Playgroud)

控制台输出是:

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

Process finished with exit code 0
Run Code Online (Sandbox Code Playgroud)

我认为默认情况下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;')
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*ers 17

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

此外,defaultdict __missing__工厂完全用C代码处理,特别是当使用其他类型时int(),它本身是用C实现的.Counter源代码是纯Python,因此该Counter.__missing__方法需要Python框架来执行.

因为dict.get()仍然在C中处理,所以构造函数方法对于a来说是更快的方法Counter(),前提是您使用相同的技巧Counter.update()使用并将self.get查找别名为本地:

>>> import timeit
>>> import random
>>> 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
Run Code Online (Sandbox Code Playgroud)

两者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
Run Code Online (Sandbox Code Playgroud)

这是一个使用别名dict.get()方法获得最大速度的常规字典.但是你还必须重新实现包的行为CounterCounter.most_common()方法.该defaultdict用例去的方式数不胜数.

在Python 3.2中,Counter()通过添加处理此案例的C库来更新速度得到提升; 见问题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
Run Code Online (Sandbox Code Playgroud)

  • 截至 2020 年,使用 python 3.7 >= 这不再是这种情况 - 计数器更快 (5认同)
  • 不需要补丁; Python 3(我不记得是什么小版本)已经加快了速度,所以调用`Counter`比你的最终代码块更快. (3认同)
  • @Veedrac:啊,的确,感谢指针.找到变更集和[原始错误报告](http://bugs.python.org/issue10667). (3认同)
  • @bones.felipe 请阅读整个答案。问题是关于 Python 2.7 的,但我提到,从 *Python 3.2* 开始,计数器更快,所以从 2012 年(8 年前)开始。 (2认同)