为什么按顺序将键插入Python字典比无序插入更快?

5

我一直在创建巨大的字典(数百万个条目),我注意到如果按顺序创建它们,速度会更快。

我想这可能与哈希函数的冲突有关,但是有人可以解释一下为什么会发生这种情况以及是否在 Python 的不同版本中一致吗?

这里有一个人工示例:

import timeit
import random

def get_test_data(num, size):
    olist, ulist = [], []
    for _ in range(num):
        otest = [str(i) for i in range(size)]
        utest = list(otest)
        random.shuffle(utest)
        olist.append(otest)
        ulist.append(utest)
    return olist, ulist

NUM_TESTS = 20
# Precalculate the test data so we only measure dict creation time
ordered, unordered = get_test_data(NUM_TESTS, 1000000)

def test_ordered():
    dict((k, k) for k in ordered.pop())

def test_unordered():
    dict((k, k) for k in unordered.pop())

print "unordered: ",
print timeit.timeit("test_unordered()",
                    setup="from __main__ import test_unordered, test_ordered",
                    number=NUM_TESTS)
print "ordered: ",
print timeit.timeit("test_ordered()",
                    setup="from __main__ import test_unordered, test_ordered",
                    number=NUM_TESTS)

我的机器输出结果一直是:

(X)$ python /tmp/test.py 
unordered:  8.60760807991
ordered:  5.1214389801

我正在使用Ubuntu Precise x86_64中的Python 2.7.3


可能有关联,但我们应该查看字典的C实现。 - barracel
2个回答

8
我几乎可以确定发生了以下情况:当您首次创建“otest”时,字符串按顺序存储在内存中。当您创建“utest”时,这些字符串指向相同的内存缓冲区,但是现在这些位置已经无序,这会影响无序测试用例的缓存性能。
这是证据。我用以下版本替换了您的“get_test_data”函数:
def get_test_data(num, size):
    olist, ulist = [], []
    for _ in range(num):
        nums = range(size)
        random.shuffle(nums)
        utest = [str(i) for i in nums]
        otest = list(utest)
        otest.sort(key=lambda x: int(x))
        olist.append(otest)
        ulist.append(utest)
    return olist, ulist

我的想法是,我现在正在按顺序在内存中构造ulist的字符串,然后使用适当的键将那些字符串排序,以构建olist。在我的机器上,这反转了两个测试的运行时间。


我的代码其余部分与@barracel的代码完全相同,只是我不得不将列表大小缩小一个数量级。我的电脑没有那么多内存:( 我在原始测试中得到了(1.25秒,0.97秒)的结果,在新测试中得到了(0.93秒,1.09秒)的结果。 - disatisfieddinosaur
在我的机器上执行您的函数后,我得到了这样的输出: “unordered: 7.00250697136 ordered: 7.96612787247”。问题在于,在原始代码中只有从磁盘读取的列表。因此,我认为我应该改进示例代码以更好地反映实际情况。 - barracel
嗯,还是有点奇怪。另一方面的差异并不那么明显...有什么想法吗? - disatisfieddinosaur
啊,这里有一个关于剩下的差异的理论。Python字符串哈希是一种滚动哈希函数,当字符串仅由它们的最后一个字符不同时,它几乎不会改变。这意味着每10个连续的数字哈希到彼此相邻的桶中,这具有稍微更好的缓存性能。(字典适合内存,但总有更小+更快的缓存级别。) - disatisfieddinosaur
我创建了一个自定义字符串,并使用随机的__hash__和标准字符串__hash__测试了代码。它证实了你的理论,使用字符串哈希创建字典更快。检查字符串__hash__的实现,它确实为内存查找提供了更好的局部性。也许你可以在你的答案中添加你的评论。 - barracel

2

检查Python字典的源代码,你可以看到连续的字符串或整数会导致更少的冲突。这与@skishore的评论有关,他提到了更好的缓存局部性可能是答案。

Major subtleties ahead: Most hash schemes depend on having a "good" hash function, in the sense of simulating randomness. Python doesn't: its most important hash functions (for strings and ints) are very regular in common cases:

>>> map(hash, (0, 1, 2, 3))
[0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
[-1658398457, -1658398460, -1658398459, -1658398462]
>>>

This isn't necessarily bad! To the contrary, in a table of size 2**i, taking the low-order i bits as the initial table index is extremely fast, and there are no collisions at all for dicts indexed by a contiguous range of ints. The same is approximately true when keys are "consecutive" strings. So this gives better-than-random behavior in common cases, and that's very desirable.


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