【问题标题】:Fast hash function for long string key长字符串键的快速哈希函数
【发布时间】:2015-12-18 09:58:39
【问题描述】:

我正在使用可扩展的哈希,并且我希望将字符串作为键。问题是我正在使用的当前哈希函数迭代整个字符串/键,我认为这对程序的性能非常不利,因为哈希函数被多次调用,尤其是在我拆分存储桶时。

当前哈希函数

int hash(const string& key)
{
    int seed = 131;
    unsigned long hash = 0;
    for(unsigned i = 0; i < key.length(); i++)
    {
        hash = (hash * seed) + key[i];
    }
    return hash;
}

键可以长达 40 个字符。

字符串/键示例

string key = "from-to condition"

我已经在互联网上搜索了一个更好的,但我没有找到任何与我的情况相匹配的东西。有什么建议吗?

【问题讨论】:

  • 40 个字符并没有那么长。并且您使用的当前哈希函数与 std 库中的哈希函数相比没有任何优势。上次我检查时,MSVC 使用 FNV1a 处理字符串,它不应该比你的慢,但对于散列来说要好得多。我没有检查,但 GCC 可能使用相同的。
  • 您的哈希值错误。它很容易溢出,整数溢出是未定义的行为。
  • 不,它不能(好吧,除非 ULONG_MAX == INT_MAX,这是一个非常病态的实现。事实上,我不认为它是合法的。)计算是在 unsigned long 中完成的哪个溢出是完美定义的。
  • 嗯,我是在网上找到的。我想我得再找一个了。
  • @UmNyobe 他的哈希值不好的事实与溢出无关。如前所述,无符号溢出是完美定义的。几乎所有的哈希算法都使用溢出。

标签: c++ hash-function


【解决方案1】:

您应该更喜欢使用std::hash,除非测量表明您可以做得更好。要限制它使用的字符数,请使用以下内容:

    const auto limit = min(key.length(), 16);
    for(unsigned i = 0; i < limit; i++)

您需要进行试验以找到 16 的最佳使用值。

我实际上预计性能会变得更糟(因为你会遇到更多的碰撞)。如果您的字符串有几个 k,那么限制到前 64 个字节可能是值得的。

根据您的字符串,可能值得不要从头开始。例如,散列文件名可能会更好地使用从末尾开始的 20 到 5 之间的字符(忽略通常不变的路径名前缀和文件扩展名)。但你仍然需要衡量。

【讨论】:

    【解决方案2】:

    我正在使用可扩展的哈希,我希望将字符串作为键。

    如前所述,使用std::hash 直到有充分的理由不使用。

    问题是我正在使用的当前哈希函数迭代整个字符串/键,我认为这很糟糕......

    这是一个可以理解的想法,但实际上不太可能成为真正的问题。

    (期待)为什么?

    快速扫描堆栈溢出会发现许多有经验的开发人员都在谈论缓存和缓存行。

    (请原谅我教奶奶吸蛋)

    现代 CPU 在处理指令和执行(非常复杂的)算术方面速度非常快。在几乎所有情况下,限制其性能的是必须通过总线与内存通信,相比之下,这非常慢。

    因此,芯片设计人员构建了内存缓存——位于 CPU 中的极快内存(因此不必通过慢速总线进行通信)。不幸的是,这个高速缓存只有这么多可用空间[加上热量限制 - 另一天的话题],因此 CPU 必须像操作系统一样处理磁盘高速缓存,在需要时刷新内存和读取内存.

    如前所述,通过总线进行通信很慢 - (简单地说)它需要主板上的所有电子组件停止并相互同步。这浪费了大量的时间[这将是一个很好的观点,可以讨论电子信号在主板上的传播受到大约一半光速的限制——这很有趣,但这里只有这么多空间,我有只有这么多时间]。因此,不是一次传输一个字节、一个字或一个长字,而是以块的形式访问内存 - 称为 缓存行

    事实证明这是芯片设计人员的一个很好的决定,因为他们知道大多数内存是按顺序访问的——因为大多数程序大部分时间都在线性访问内存(例如在计算哈希、比较字符串或对象、转换序列,复制和初始化序列等)。

    这一切的结果是什么?

    好吧,奇怪的是,如果你的字符串还没有在缓存中,那么读取它的一个字节几乎与读取第一个(比如说)128 字节中的所有字节一样昂贵。

    另外,由于缓存电路假定内存访问是线性的,它会在获取您的第一个缓存行后立即开始获取下一个缓存行。它会在您的 CPU 执行其哈希计算时执行此操作。

    我希望你能看到,在这种情况下,即使你的字符串有数千个字节长,并且你选择只散列(比如说)每 128 个字节,你要做的就是计算一个非常差的哈希仍然导致内存缓存在它获取大块未使用的内存时停止处理器。 需要同样长的时间 - 结果更糟!

    话虽如此,不使用标准实现的充分理由是什么?

    仅当:

    1. 用户抱怨您的软件太慢而无法使用,并且

    2. 程序可验证受 CPU 限制(使用 100% 的 CPU 时间),并且

    3. 程序不会通过旋转浪费任何周期,并且

    4. 仔细分析发现,程序最大的瓶颈是哈希函数,

    5. 另一位经验丰富的开发人员的独立分析证实,没有办法改进算法(例如通过减少调用哈希)。

    简而言之,几乎从来没有。

    【讨论】:

    • 请注意,使用 100% 的 CPU 时间并不意味着程序受 CPU 限制。当 CPU 因高速缓存或内存访问而停顿时,仍将计为 CPU 时间。您需要比“%”更深入的分析来区分 CPU 密集型和内存密集型。
    【解决方案3】:

    你可以直接使用std::hashlink,而不是自己实现函数。

    #include <iostream>
    #include <functional>
    #include <string>
    
    size_t hash(const std::string& key)
    {
        std::hash<std::string> hasher;
        return hasher(key);
    }
    
    int main() {
        std::cout << hash("abc") << std::endl;
        return 0;
    }
    

    在此处查看此代码:https://ideone.com/4U89aU

    【讨论】:

    • 这是一个很好的出发点,但这是实现定义的,并且 if 太慢了一些关于如何编写快速(er)哈希函数的提示可能是明智的...... ?
    • @TonyDelroy 我确信在寻找更快的哈希函数方面有大量的计算机科学工作可供 phd 使用。每个人都相信他们是最好的。您将需要一个完整的独立网站,其中包含测试用例和详细讨论,这需要数年时间才能成熟。或者您可以只使用 std::hash
    • @BoppityBop: 都是真的,但是这个特定的问题要求一个字符串散列函数,它不会遍历整个字符串 (..." 在整个字符串/键上,我认为这是对程序的性能非常不利”)。使用std::hash&lt;&gt; 就是放弃对该实现方面的控制。碰巧 GCC 和 clang 的实现将遍历整个字符串,而 Micrtosoft Visual C++ 的标准库不会(但它所做的 - 在整个字符串中均匀地选取 10 个字符,对约 40 个字符串没有帮助)。好像你说 Q 太难了?
    猜你喜欢
    • 2012-03-21
    • 2014-03-26
    • 1970-01-01
    • 2013-05-02
    • 2013-09-12
    • 2012-10-03
    • 2011-08-17
    • 2015-03-09
    相关资源
    最近更新 更多