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


我用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()。


1239
2018-01-06 15:35


起源



答案:


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

而且, defaultdict  __missing__ 工厂完全用C代码处理,特别是在使用其他类型时 int() 这本身就是用C.实现的 Counter source是纯Python而且就是这样 Counter.__missing__ 方法需要Python框架才能执行。

因为 dict.get() 仍然在C中处理,构造函数方法是一种更快的方法 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

defaultdict 和 Counter 是为他们的功能而不是他们的表现而建立的有用课程;不依赖于 __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中,更新一个 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

15
2018-01-06 15:49



我做了另一个测试,使用dict()比defaultdict()更快,将更新问题 - BlueTrin
这确实令人惊讶;你期望一个专门构建的类是最快的实现。为什么不呢 Counter 使用 defaultdict 它的实施是否更快? - Mark Ransom
@MarkRansom: Counter 不止于此 defaultdict 确实。但如果我们能创造一个 Counter 作为一个 defaultdict 子类更快,那么也许你可以提出一个补丁。 :-) - Martijn Pieters♦
不需要补丁; Python 3(我不记得是什么小版本)已经加快了这一速度 Counter 比最后的代码块更快。 - Veedrac
@Veedrac:啊,的确,感谢指针。找到了变更集和 原始的bug报告。 - Martijn Pieters♦


答案:


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

而且, defaultdict  __missing__ 工厂完全用C代码处理,特别是在使用其他类型时 int() 这本身就是用C.实现的 Counter source是纯Python而且就是这样 Counter.__missing__ 方法需要Python框架才能执行。

因为 dict.get() 仍然在C中处理,构造函数方法是一种更快的方法 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

defaultdict 和 Counter 是为他们的功能而不是他们的表现而建立的有用课程;不依赖于 __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中,更新一个 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

15
2018-01-06 15:49



我做了另一个测试,使用dict()比defaultdict()更快,将更新问题 - BlueTrin
这确实令人惊讶;你期望一个专门构建的类是最快的实现。为什么不呢 Counter 使用 defaultdict 它的实施是否更快? - Mark Ransom
@MarkRansom: Counter 不止于此 defaultdict 确实。但如果我们能创造一个 Counter 作为一个 defaultdict 子类更快,那么也许你可以提出一个补丁。 :-) - Martijn Pieters♦
不需要补丁; Python 3(我不记得是什么小版本)已经加快了这一速度 Counter 比最后的代码块更快。 - Veedrac
@Veedrac:啊,的确,感谢指针。找到了变更集和 原始的bug报告。 - Martijn Pieters♦