【问题标题】:Why inserting keys in order into a python dict is faster than doint it unordered为什么按顺序将键插入python dict比无序插入要快
【发布时间】:2013-08-14 06:39:07
【问题描述】:

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

我认为它与哈希函数的冲突有关,但有人可以解释为什么会发生这种情况以及它在 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 精确 x86_64 中使用 Python 2.7.3

【问题讨论】:

标签: python performance dictionary


【解决方案1】:

我几乎可以肯定这是怎么回事:当您第一次创建 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.25s, 0.97s) 和新的 (0.93s, 1.09s)。
  • 你和你一起在我的机器上运行我得到:“无序:7.00250697136 有序:7.96612787247。”问题是在原始代码中只有一个从磁盘读取的列表。所以我认为我应该改进示例代码以更好地反映情况。
  • 嗯。还是有什么奇怪的。另一种方式的差异没有那么戏剧性......想法?
  • 啊。这是其余差异的理论。 Python 字符串哈希是rolling hash function,当字符串仅在最后一个字符不同时变化不大。这意味着每 10 个连续数字散列到靠得很近的存储桶中,这具有稍微更好的缓存性能。 (字典适合内存,但总是有更小+更快的缓存级别。)
  • 我创建了一个自定义字符串并使用随机 hash 和标准字符串 hash 测试了代码。它证实了你的理论,使用字符串哈希创建字典更快。检查字符串 hash 的实现,它确实为内存查找提供了更好的局部性。也许您可以将您的评论添加到您的答案中。
【解决方案2】:

检查source code of the python dict 您可以看到连续的字符串或整数产生的冲突更少。这与@skishore 关于更好的缓存局部性的评论相结合可能是答案。

前面的主要细节:大多数哈希方案都依赖于“好” 散列函数,在模拟随机性的意义上。 Python 不会: 它最重要的哈希函数(用于字符串和整数)非常 普通情况下的常规:

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

这不一定是坏事!相反,在大小为 2**i 的表中, 将低位 i 位作为初始表索引是极 快速,并且对于由 a 索引的 dicts 根本没有冲突 连续的整数范围。当键是 “连续”的字符串。所以这给出了比随机更好的行为 常见的情况,这是非常可取的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-13
    • 2023-02-05
    • 2016-02-01
    • 2020-04-05
    • 1970-01-01
    • 2018-11-20
    • 2022-06-11
    相关资源
    最近更新 更多