没有并集的哈希代码的内存地址

Memory address to hashcode without union

本文关键字:代码 内存 地址 哈希      更新时间:2023-10-16

在学习数据结构,特别是哈希表时,我们被告知为数据类型发明一个有效的哈希函数是一项非常艰巨的任务,但有人建议存在一个快速快捷方式。也就是说,如果我们可以假设对象在内存中不移动,并且我们可以将对象相等定义为具有相同的内存地址(使用引用相等而不是值相等),那么我们可以获得这样的对象的哈希代码:

#include<iostream>
template<typename T>
class hashcoder {
private:
    union impl {
        T* ptr;
        int32_t hashcode; // or int64_t on a 64-bit architecture = architecture with 64-bit pointers
    };
public:
    static int32_t hash(const T& r) {
        impl i;
        i.ptr = &r;
        return i.hashcode;
    }
};
class myclass {
  // whatever
};
int main() {
    myclass m;
    std::cout << hashcoder<myclass>::hash(m) << std::endl;
}

所以我的问题是:

  • 使用内存地址作为哈希代码有什么问题吗(再次假设引用相等是所需的行为)
  • 考虑到使用并集进行转换是未定义的行为,我们如何将内存地址转换为整数
  • (请随意指出我在上面代码中犯的任何其他错误。C++指针很容易出错。)
  • 不,这没什么错;散列是一个整数,保证每个对象都是唯一的,这降低了冲突的概率
  • 将指针投射到uintptr_t。不需要工会。此外,uintptr_t具有适合平台的大小,因此您不再需要使用int32_t

    uintptr_t hash(const T &r)
    {
        return uintptr_t(&r);
    }
    

(如果散列必须是32位,则将其强制转换为uint32_t,或者在64位平台上,使用适当的魔术将两半结合起来。)

首先,您的代码假设T的不同实例总是不同的(对于!=运算符)。这种情况并不总是如此,例如,当你想对字符串值(即内容)进行散列,而不是对某个地址进行散列时,std::string肯定是错误的。。。。同样,如果你想散列整数的(数学)向量,你应该散列它们的内容(向量的数学组成部分)。

然后,您应该意识到,仅仅使用某个对象的地址作为其哈希可能是次优的,因为足够大的对象的地址往往是8或16字节的倍数(确切地说,是该对象类型的对齐方式),并且在同一时刻分配的对象地址通常非常相似。事实上,指针的一些"中间位"可能比低位或高位更"随机"。

一个稍微好一点的方法可能是对指针地址进行一些逐位运算,例如

static inline int32_t ptrhash(const void*p) {
   uintptr_t u = (uintptr_t)p;
   return u ^ (u >> 10);
}

static inline int32_t ptrhash(const void*p) {
   uintptr_t u = (uintptr_t)p;
   return (u * 200771) + (u % 300823);
}

200771和300823都是素数。您可以用逐位异或^替换+

或者适当地混合地址的比特的任何技巧。

当然,YMMV和它绝对是系统特定的(例如,取决于您的系统是否有ASLR)

对于一些class T,另一种方法可能是在构造函数中生成一些随机哈希(使用类似lrand48的quickPRNG…),并将该哈希作为私有实例成员变量。或者简单地使用一些静态计数器,对每个实例进行唯一编号,并将该编号用作哈希。

重要的一点是要确保您的所有实例都是不同的!如果你想对内容进行散列,情况并非如此(但请参阅记忆等…)

此外,我不同意你的老师:事实上,发明一个非常好的哈希函数很难,但制作一个"足够好"的平均哈希函数通常很容易(例如,使用与组成部分哈希的素数系数的线性组合,见Bezout定理)。通常,在实践中,这种"简单"的哈希函数工作得足够好(当然也有例外,最坏的情况可能令人敬畏)。

还请阅读有关完美散列的内容,例如使用GNU gperf。

如果您想要引用相等,那么使用地址作为哈希码没有错。然而,实现它的一种比联合更明智的方法是使用intptr_t。如果intptr_t大于int32_t,则可以与-1进行AND运算,然后将static_castuint32_t进行AND运算。

您只需要一个函数模板。以下是你可以做的(假设你的哈希表有有限数量的桶):

template <typename T>
uintptr_t hashcode(const T &obj, size_t size) {
    return reinterpret_cast<uintptr_t>(&obj) % size;
}