【问题标题】:Efficient C++ API for an Open Addressing Hash Table用于开放寻址哈希表的高效 C++ API
【发布时间】:2017-05-11 14:47:45
【问题描述】:

我正在构建一个 C++ 开放寻址哈希表。它由一个数组组成:

struct KeyValue {
    K key;
    V value;
}

Key 类型有两个特殊元素:空和墓碑。第一个用来说明slot是空闲的,第二个用来说明slot已经被使用过但后来又被删除了(探测需要)。

主要的挑战是为此结构设计一个高效的 API。我想最小化一个键被散列和寻找一个槽的次数。

到目前为止,我发现以下 API 不安全:

// Return the slot index if the key is in the table
// or a slot index where I can construct the KeyValue
// if the key is not here (or -1 if there is no slot
// available and the insertion of such a key would
// need to grow the hash table)
int search(const K& key)

// Tells if the slot is empy (or if i == -1)
bool empty(int i)

// Construct a KeyValue in the HashTable in the slot i
// which has been found by search. The i might be changed
// if the table needs to grow.
void insert(const K& key, const V& value, int& i)

// Accessors for a slot i which is occupied
const V& value(int i);

注意,表格也有经典的功能如

void insert(const K& key, const V& value)

它计算哈希,搜索一个槽,并将对插入到表中。但我想在这里集中讨论允许程序员非常有效地使用表格的界面。

例如,这里有一个函数,如果 f(key) 从未计算过,则返回它的值;如果 f(key) 已经计算过,则返回其值。

const V& compute(const K& key, HashTable<K, V>& table) {
    int i = table.search(key);
    if (table.empty(i)) {
        table.insert(key, f(key), i);
    }
    return table.value(i);
 }

我并不完全热衷于这个 HashTable 的接口,因为方法 insert(const K&, const V&, int&) 对我来说真的很不安全。

您对更好的 API 有什么建议吗?

PS:Chandler Carruth 的演讲“算法的性能,数据结构的效率”,特别是在 23:50 之后,非常好理解 std::unordered_map 的问题

【问题讨论】:

  • 你的目标是什么?表现?内存使用情况?无论如何,您能详细说明为什么std::unordered_map 不够用吗?
  • 我们的目标是得到一个高性能的HashTable。 std::unordered_map 的问题之一是解决了与链表的冲突,这从性能角度来看是不利的。另外,尝试使用 std::unordered_map 编写“计算”。
  • 知道了。我也很困惑为什么要将“插槽索引”公开给插入的调用者。因为哈希表的全部意义在于您可以通过“key”插入一些东西,然后通过“key”查找它。如果您要向调用者返回一个槽索引以供后续查找,那么您的实现也可能只是一个带有增量索引的平面数组。但我认为您真正想要的只是公开三种方法:void Insert(k,v)void Remove(k)v Lookup(k)
  • 另外,几年前我编写了自己的哈希表类,目标是在实例构建后不再分配内存。因为在服务器端,内存分配会损害性能。欢迎您参考或使用。 It's here on GitHubunit tests
  • selbie:如果你想编写一个只对密钥进行一次哈希处理的“计算”函数,则需要返回 i。显然,哈希表的常规用法是稍后请求值并通过哈希函数完成。感谢您在 github 上的参考。

标签: c++ api hash hashtable unordered-map


【解决方案1】:

您可以创建一个get_or_insert 函数模板,它接受任意函子而不是值。然后您可以使用 lambda 调用它:

template <class K, class V>
class HashTable {
private:
    int search(const K& key);
    bool empty(int i);
    void insert(const K& key, const V& value, int& i);
    const V& value(int i);

public:    
    template <class F>
    const V& get_or_insert(const K& key, F&& f) {
        int i = search(key);
        if (empty(i)) {
            insert(key, f(), i);
        }
        return value(i);
    }
};

double expensive_computation(int key);

void foo() {
    HashTable<int, double> ht;
    int key = 42;
    double value = ht.get_or_insert(key, [key]{ return expensive_computation(key); });
}

如果get_or_insert 是内联的并且您不需要捕获很多内容,那么这应该与您显示的代码一样有效。如有疑问,请使用 Godbolt 的 Compiler Explorer 或类似工具比较生成的代码。 (如果它没有被内联,它仍然可以,除非您必须捕获许多不同的变量。假设您捕获智能 - 即如果复制成本很高,则通过引用捕获内容。)

注意:在 C++ 中传递函子的“标准”方式似乎是按值传递,但我认为按引用传递更有意义。万一一切都被内联了,它不应该有所作为(并且在我使用 GCC、Clang 和 MSVC 检查的示例中没有),并且如果 get_or_insert 调用没有被内联,你真的没有如果函子捕获超过 1 或 2 个小而平凡的变量,则希望复制该函子。

我能想象的使用通用引用的唯一缺点是,如果你有一个仿函数,它会改变它在operator() 中的状态。对于这样的函子,至少在我能想到的例子中,我希望原始的函子发生变异。所以不是真正的缺点 IMO。


或上述的修改版本,适用于创建/分配/销毁值昂贵的情况(如std::string):使用对槽中值的可变引用调用函子。然后仿函数可以直接分配/改变哈希表中的值 -> 无需构造和销毁临时。

【讨论】:

    【解决方案2】:

    我认为你应该尝试超快速散列函数。

    查看https://github.com/Cyan4973/xxHash。我引用它的描述:“xxHash 是一种极快的哈希算法,在 RAM 速度限制下运行。它成功完成了评估哈希函数的碰撞、分散和随机性质量的 SMHasher 测试套件。代码高度可移植,并且哈希在所有平台(小/大端)。”

    还有来自本网站另一个问题的主题:Fast Cross-Platform C/C++ Hashing Library。众所周知,FNV、Jenkins 和 MurmurHash 速度很快。

    看看这个帖子,我在这里发布了相同的答案,那里也有其他答案: Are there faster hash functions for unordered_map/set in C++?

    【讨论】:

      猜你喜欢
      • 2011-02-03
      • 1970-01-01
      • 1970-01-01
      • 2016-01-29
      • 1970-01-01
      • 2011-11-17
      • 2018-09-17
      • 1970-01-01
      • 2019-03-31
      相关资源
      最近更新 更多