【问题标题】:Counting collisions in a Python dictionary在 Python 字典中计算碰撞
【发布时间】:2011-06-19 09:59:25
【问题描述】:

我第一次在这里发帖,所以希望我以正确的方式提出了我的问题,

将元素添加到 Python 字典后,是否可以让 Python 告诉您添加该元素是否会导致冲突? (以及在找到放置元素的位置之前,碰撞解决策略探测了多少个位置?)

我的问题是:我在一个更大的项目中使用字典,经过大量分析后,我发现代码中最慢的部分是处理使用字典实现的稀疏距离矩阵。

我使用的键是 Python 对象的 ID,它们是唯一的整数,所以我知道它们都散列到不同的值。但是原则上将它们放入字典仍然可能导致冲突。我不认为字典冲突会拖慢我的程序,但我想从查询中消除它们。

因此,例如,给定以下字典:

d = {}
for i in xrange(15000):
    d[random.randint(15000000, 18000000)] = 0

你能让 Python 告诉你在创建它时发生了多少次冲突吗?

我的实际代码与应用程序纠缠在一起,但上面的代码生成的字典看起来与我正在使用的字典非常相似。

重复一遍:我不认为碰撞会降低我的代码速度,我只是想通过证明我的字典没有太多碰撞来消除这种可能性。

感谢您的帮助。

编辑:一些代码来实现@Winston Ewert 的解决方案:

n = 1500
global collision_count
collision_count = 0

class Foo():

    def __eq__(self, other):
        global collision_count
        collision_count += 1
        return id(self) == id(other)

    def __hash__(self):
        #return id(self) # @John Machin: yes, I know!
        return 1

objects = [Foo() for i in xrange(n)]

d = {}
for o in objects:
    d[o] = 1

print collision_count

请注意,当您在类上定义 __eq__ 时,如果您没有同时定义 __hash__ 函数,Python 会为您提供 TypeError: unhashable instance。

它没有像我预期的那样运行。如果你有__hash__ 函数return 1,那么你会得到大量的冲突,正如预期的那样(在我的系统上,n=1500 有 1125560 次冲突)。但是对于return id(self),有 0 次冲突。

有人知道为什么说 0 次碰撞吗?

编辑: 我可能已经想通了。

是不是因为 __eq__ 仅在两个对象的 __hash__ 值相同时才被调用,而不是它们的“压缩版本”(如@John Machin 所说)?

【问题讨论】:

  • 您的意思是您想知道内部 dict 算法是否通过任何哈希表探测来查找元素?这就是你所说的“碰撞”吗?
  • 一些半相关信息:hash(-1)==hash(-2)。除此之外,区间 -sys.maxint-1 <= x <= sys.maxint 中的所有整数 x 都具有唯一的哈希值。这里描述了散列长整数的算法:effbot.org/zone/python-hash.htm
  • "哈希值 -1 是保留的(用于标记 C 实现中的错误)。如果哈希算法生成此值,我们只需使用 -2 代替。哎哟。
  • @unutbu:(1) OP 的键是对象 ID,而不是长整数。不一样的故事。 (2) 城市神话:唯一的哈希意味着没有冲突。是错的。看我的回答。
  • @S.Lott:是的,这正是我的意思。

标签: python optimization dictionary


【解决方案1】:

这个想法实际上行不通,请参阅问题中的讨论。

快速浏览一下 python 的 C 实现会发现,解决冲突的代码不会计算或存储冲突的数量。

但是,它会在键上调用PyObject_RichCompareBool 以检查它们是否匹配。这意味着每次碰撞都会调用键上的__eq__。

所以:

用定义__eq__ 的对象替换您的键,并在调用时增加一个计数器。由于跳转到 python 进行比较所涉及的开销,这会更慢。但是,它应该让您了解发生了多少次碰撞。

确保你使用不同的对象作为key,否则python会走捷径,因为一个对象总是等于它自己。此外,请确保对象散列到与原始键相同的值。

【讨论】:

  • 这是一个非常酷的想法 - 谢谢!我会试试看,与@John Machin 的实验相比,我得到了多少碰撞。
  • 我已经尝试过你的建议,但它并没有达到我的预期!请参阅我的问题中的编辑。
  • 一点 sn-p 在这里的帮助不仅仅是解释如何做。
【解决方案2】:

简答:

您不能通过使用随机整数作为字典键来模拟使用对象 ID 作为字典键。它们具有不同的哈希函数。

确实会发生碰撞。 “拥有独特的事物意味着没有冲突”对于“事物”的几个值是错误的。

您不必担心碰撞。

长答案:

一些解释,来源于reading the source code:

一个 dict 被实现为一个包含 2 ** i 个条目的表,其中 i 是一个整数。

dicts 不超过 2/3。因此,对于 15000 个键,i 必须是 15,而 2 ** i 是 32768。

当 o 是未定义 __hash__() 的类的任意实例时,hash(o) == id(o) 是不正确的。由于地址的低 3 位或 4 位可能为零,因此通过将地址右移 4 位来构造哈希;见source file Objects/object.c,函数_Py_HashPointer

如果低位中有很多零,那将是一个问题,因为要访问大小为 2 ** i(例如 32768)的表,必须对哈希值(通常远大于该值)进行处理以适应,这可以通过获取哈希值的低阶 i(例如 15)位来非常简单快速地完成。

因此碰撞是不可避免的。

不过,这不是恐慌的原因。哈希值的剩余位被考虑到下一次探测的位置的计算中。需要进行第三次等探测的可能性应该很小,特别是因为字典永远不会超过 2/3。计算第一个和后续探针的时隙的廉价成本降低了多个探针的成本。

下面的代码是一个简单的实验,说明了上述大部分讨论。它假定字典在达到其最大大小后随机访问。在 Python2.7.1 中,它显示了 15000 个对象的大约 2000 次碰撞 (13.3%)。

无论如何,底线是您应该真正将注意力转移到其他地方。碰撞不是你的问题,除非你已经为你的对象取得了一些非常不正常的获取内存的方法。你应该看看你是如何使用字典的,例如使用k in d 或try/except,而不是d.has_key(k)。考虑一个作为d[(x, y)] 访问的字典,而不是作为d[x][y] 访问的两个级别。如果您需要这方面的帮助,请提出一个单独的问题。

更新在 Python 2.6 上测试后:

直到 Python 2.7 才引入循环地址​​;请参阅this bug report 进行全面讨论和基准测试。恕我直言,基本结论仍然有效,并且可以通过“如果可以更新”来补充。

>>> n = 15000
>>> i = 0
>>> while 2 ** i / 1.5 < n:
...    i += 1
...
>>> print i, 2 ** i, int(2 ** i / 1.5)
15 32768 21845
>>> probe_mask = 2 ** i - 1
>>> print hex(probe_mask)
0x7fff
>>> class Foo(object):
...     pass
...
>>> olist = [Foo() for j in xrange(n)]
>>> hashes = [hash(o) for o in olist]
>>> print len(set(hashes))
15000
>>> probes = [h & probe_mask for h in hashes]
>>> print len(set(probes))
12997
>>>

【讨论】:

  • 这很好 - 谢谢!这一切都非常有帮助。好的,我有两个问题/cmets:(1)我没有在字典中添加“o”(其中 o 是对象的实例),而是添加 id(o)。据推测,这不会将地址向右旋转 4 位,并且可能会给我带来更多预期的冲突。如果是这样,我应该使用 o 而不是 id(o)。 (2) 我正在使用两个级别的 dicts:d[x][y],因为对于给定的 x,我想遍历它的所有邻居(所有 y)。如果您使用 d[(x, y)],这样做会很快吗?如果更合适的话,我可以将其作为一个单独的问题发布。
  • @Adam Nellis: (1) 使用 id(o) 而不是 o 作为 dict 键是在浪费函数调用并得到一个不可能更好而且可能更糟的结果。 (2) 不可以。您必须遍历所有项目并忽略 x 值不感兴趣的项目。
  • @John Machin:感谢您的帮助。通过阅读 Objects/object.c,我相信你对 hash(o) != id(o) 的看法,但是当我打印出 [hash(o) for o in olist] 和 [id(o) for o in olist] 时,他们是一样的。我错过了什么吗? (我正在运行 Python 2.6.2)
  • @John Machin:另外,我认为您的回答中有轻微的打字错误。你在“12997”中有一个额外的数量级,因为这不是 15000 的 13.3%。运行你的代码,我得到了相似百分比的碰撞(我的系统上有 6.8% 的碰撞)。
  • @John Machin: (2) 哎呀,是的,对不起——我是个白痴! (1) 哇,是的 - 在 Python 2.6 和 Python 2.7 上运行它之后(并计算碰撞,而不是唯一的探针!)我在 Python 2.6(或使用 id(o) 的 Python 2.7)上得到 93% 的碰撞,但只有 17% Python 2.7 上的冲突(使用 hash(o))!所以是的,碰撞是个问题。我已经更新到 2.7 并重新编写了我的代码以散列对象实例而不是它们的 ID。 (虽然它仍然运行得太慢,但没有以前那么慢了:))
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-23
  • 1970-01-01
  • 2017-08-24
  • 2011-04-12
  • 1970-01-01
  • 1970-01-01
  • 2012-01-05
相关资源
最近更新 更多