【问题标题】:Python frozenset hashing algorithm / implementationPythonfrozenset散列算法/实现
【发布时间】:2014-01-16 21:50:31
【问题描述】:

我目前正在尝试了解为 Python 的内置 frozenset 数据类型定义的哈希函数背后的机制。实现显示在底部以供参考。我特别感兴趣的是选择这种散射操作的基本原理:

lambda h: (h ^ (h << 16) ^ 89869747) * 3644798167

其中h 是每个元素的哈希值。有谁知道这些是从哪里来的? (也就是说,选择这些数字有什么特别的原因吗?)还是他们只是随意选择的?


这是来自官方 CPython 实现的 sn-p,

static Py_hash_t
frozenset_hash(PyObject *self)
{
    PySetObject *so = (PySetObject *)self;
    Py_uhash_t h, hash = 1927868237UL;
    setentry *entry;
    Py_ssize_t pos = 0;

    if (so->hash != -1)
        return so->hash;

    hash *= (Py_uhash_t)PySet_GET_SIZE(self) + 1;
    while (set_next(so, &pos, &entry)) {
        /* Work to increase the bit dispersion for closely spaced hash
           values.  The is important because some use cases have many
           combinations of a small number of elements with nearby
           hashes so that many distinct combinations collapse to only
           a handful of distinct hash values. */
        h = entry->hash;
        hash ^= (h ^ (h << 16) ^ 89869747UL)  * 3644798167UL;
    }
    hash = hash * 69069U + 907133923UL;
    if (hash == -1)
        hash = 590923713UL;
    so->hash = hash;
    return hash;
}

还有一个equivalent implementation in Python

def _hash(self):
    MAX = sys.maxint
    MASK = 2 * MAX + 1
    n = len(self)
    h = 1927868237 * (n + 1)
    h &= MASK
    for x in self:
        hx = hash(x)
        h ^= (hx ^ (hx << 16) ^ 89869747)  * 3644798167
        h &= MASK
    h = h * 69069 + 907133923
    h &= MASK
    if h > MAX:
        h -= MASK + 1
    if h == -1:
        h = 590923713
    return h

【问题讨论】:

  • 编辑:我删除了关于为什么哈希组合函数是关联和交换的问题:由于分散元素哈希与累加器进行异或,因为异或必须如此!傻我。

标签: python hash set python-internals


【解决方案1】:

正在解决的问题是,Lib/sets.py 中以前的哈希算法在许多图形算法(其中节点表示为 frozensets):

# Old-algorithm with bad performance

def _compute_hash(self):
    result = 0
    for elt in self:
        result ^= hash(elt)
    return result

def __hash__(self):
    if self._hashcode is None:
        self._hashcode = self._compute_hash()
    return self._hashcode

创建了一种新算法,因为它具有更好的性能。以下是新算法主要部分的概述:

1) h ^= (hx ^ (hx &lt;&lt; 16) ^ 89869747) * 3644798167 中的 xor-equal 是必要的,因此算法是 commutative (哈希不依赖于遇到集合元素的顺序)。由于集合具有无序相等性测试,frozenset([10, 20]) 的哈希值需要与 frozenset([20, 10]) 的哈希值相同。

2) 选择与89869747 的异或是因为它有趣的位模式101010110110100110110110011 用于在乘以3644798167 之前分解附近哈希值的序列,3644798167 是一个随机选择的大素数和另一个有趣的位模式。

3) 包含hx &lt;&lt; 16 的异或,这样低位有两次机会影响结果(导致附近哈希值更好地分散)。在这篇文章中,我受到CRC algorithms 如何将位重排回自己的启发。

4) 如果我没记错的话,唯一特殊的常量之一是 69069。它有一些来自linear congruential random number generators 世界的历史。参考https://www.google.com/search?q=69069+rng

5) 添加了计算hash = hash * 69069U + 907133923UL 的最后一步,以处理嵌套冻结集的情况,并使算法分散在与其他对象(字符串、元组、整数等)的哈希算法正交的模式中。

6) 大多数其他常数是随机选择的大素数。

虽然我想声称哈希算法的灵感来自于上帝,但事实是我拿了一堆性能不佳的数据集,分析了为什么它们的哈希没有分散,然后玩弄算法直到碰撞统计数据停止好尴尬。

例如,这里有一个来自 Lib/test/test_set.py 的功效测试,它对于扩散较少的算法失败了:

def test_hash_effectiveness(self):
    n = 13
    hashvalues = set()
    addhashvalue = hashvalues.add
    elemmasks = [(i+1, 1<<i) for i in range(n)]
    for i in xrange(2**n):
        addhashvalue(hash(frozenset([e for e, m in elemmasks if m&i])))
    self.assertEqual(len(hashvalues), 2**n)

其他失败示例包括字符串的幂集和小整数范围以及测试套件中的图形算法:请参阅 Lib/test/test_set.py 中的 TestGraphs.test_cuboctahedron 和 TestGraphs.test_cube。

【讨论】:

  • 我在 Raymond 处理此问题时向他提供了一些测试代码。对于集合的各种嵌套模式,它随机生成数千个集合,并将冲突的数量与理想散列的预期数量进行比较。我还提供了一个初始哈希实现。它与 Raymond 的相似,但有多个步骤,并且选择的常数较少。 Raymond 的最终结果显着提高了速度和简单性,并且在随机性测试中的表现几乎一样。
【解决方案2】:

除非 Raymond Hettinger(代码的作者)介入,否则我们永远无法确定 ;-) 但这些东西中的“科学”通常比你想象的要少:你需要一些一般原则和一个测试套件,并几乎任意调整常量,直到结果看起来“足够好”。

一些一般原则“显然”在这里起作用:

  1. 要获得所需的快速“位分散”,您需要乘以一个大整数。由于 CPython 的哈希结果在许多平台上必须适合 32 位,因此需要 32 位的整数最适合此操作。而且,确实,(3644798167).bit_length() == 32

  2. 为避免系统地丢失低位,您需要乘以一个奇数。 3644798167 是奇数。

  3. 更一般地说,为了避免输入哈希中的复合模式,您希望乘以一个素数。而 3644798167 是素数。

  4. 您还需要一个二进制表示没有明显重复模式的乘法器。 bin(3644798167) == '0b11011001001111110011010011010111'。这很糟糕,这是一件好事;-)

其他常量在我看来完全是任意的。

if h == -1:
    h = 590923713

part 需要另一个原因:在内部,CPython 从整数值 C 函数中获取 -1 返回值,表示“需要引发异常”;即,这是一个错误返回。所以你永远不会在 CPython 中看到任何对象的哈希码 -1。返回的值而不是 -1 完全是任意的 - 它只需要每次都是相同的值(而不是 -1)。

编辑:玩弄

我不知道 Raymond 用什么来测试这个。这是我会使用的:查看一组连续整数的所有子集的哈希统计信息。这些是有问题的,因为hash(i) == i 有很多整数i

>>> all(hash(i) == i for i in range(1000000))
True

简单地对哈希进行异或运算会在这样的输入上产生大量取消。

所以这里有一个小函数来生成所有子集,另一个是对所有哈希码进行简单的异或:

def hashxor(xs):
    h = 0
    for x in xs:
        h ^= hash(x)
    return h

def genpowerset(xs):
    from itertools import combinations
    for length in range(len(xs) + 1):
        for t in combinations(xs, length):
            yield t

然后是一个驱动程序,以及一个显示碰撞统计信息的小函数:

def show_stats(d):
    total = sum(d.values())
    print "total", total, "unique hashes", len(d), \
          "collisions", total - len(d)

def drive(n, hasher=hashxor):
    from collections import defaultdict
    d = defaultdict(int)

    for t in genpowerset(range(n)):
        d[hasher(t)] += 1
    show_stats(d)

使用简单粗暴的哈希器是灾难性的:

>> drive(20)
total 1048576 unique hashes 32 collisions 1048544

哎呀! OTOH,使用为frozensets 设计的_hash() 在这种情况下做得很好:

>>> drive(20, _hash)
total 1048576 unique hashes 1048576 collisions 0

然后,您可以使用它来查看在_hash() 中什么 - 和不 - 产生了真正的影响。例如,如果

    h = h * 69069 + 907133923

被删除。我不知道为什么那条线在那里。同样,如果内部循环中的^ 89869747 被删除,它会继续在这些输入上做得很好——也不知道为什么会这样。并且可以从以下位置更改初始化:

    h = 1927868237 * (n + 1)

到:

    h = n

这里也没有伤害。这一切都符合我的预期:由于已经解释过的原因,内循环中的乘法常数至关重要。例如,将 1 添加到它(使用 3644798168),然后它不再是素数或奇数,并且统计信息会降级为:

total 1048576 unique hashes 851968 collisions 196608

仍然相当可用,但肯定更糟。把它改成一个小的素数,比如 13,结果更糟:

total 1048576 unique hashes 483968 collisions 564608

使用具有明显二进制模式的乘数,例如0b01010101010101010101010101010101,甚至更糟:

total 1048576 unique hashes 163104 collisions 885472

到处玩!这些东西很有趣:-)

【讨论】:

  • 感谢您对算法的分析! :)
【解决方案3】:

(h ^ (h << 16) ^ 89869747) * 3644798167

乘法整数是一个大素数以减少冲突。这一点尤其重要,因为运算是在模下进行的。

其余的可能是任意的;我认为89869747 没有具体的理由。你会得到的最重要的用途是扩大小数字的散列(大多数整数散列到它们自己)。这可以防止小整数集的高冲突。

这就是我能想到的。你需要这个做什么?

【讨论】:

  • 主要是好奇心。需要一种方法来散列具有关联和交换分支的树(即每个节点都是一个无序的多重集:Data.HashMap),并且想看看 Python 如何处理冻结集的散列。
猜你喜欢
  • 1970-01-01
  • 2015-11-23
  • 2017-11-14
  • 2012-09-03
  • 2021-02-17
  • 1970-01-01
  • 2014-06-04
  • 2012-02-25
  • 1970-01-01
相关资源
最近更新 更多