【问题标题】:Unordered map for unique keys and hashing唯一键和散列的无序映射
【发布时间】:2014-10-30 09:01:53
【问题描述】:

我在一个库中工作,其中一些在这里无关紧要的元素必须用名称标识(即值与名称相关联)。名称是用户的字符串,无论其内部表示是什么,并且应该表现得透明。

  • 名称是常量并使用字符串字面量进行初始化。它们在编译时是已知的。
  • 使用不同字符串初始化的两个名称必须比较不同,无论它们的内部表示是什么。
  • 名称可以是任意长度。我的图书馆没有设置任何限制。
  • 实际上,可能的名称不能有限制。实现的约束不能影响接口,因此,我这边也没有限制。

考虑到会频繁查找,我考虑过使用无序映射。

无序关联容器通过数字(通常为std::size_t 类型)存储其元素,无论其类型是什么,这些都是通过散列函数获得的。这意味着:

  • 对于可能值的数量小于或等于哈希值数量的类型,不应发生冲突。
  • 对于可能值的数量大于散列值的类型,可能会发生冲突,因为在散列过程中会丢失一些数据。

我想到了两种解决方案。

按值散列

使用数据本身来计算哈希值。注意事项:

  • 可能在编译时计算。 由于名称是从字符串文字构造的,因此构造函数(即 constexpr 本身)可以调用 constexpr 散列函数,并将散列值存储在类本身,以便稍后(通过哈希对象)快速检索。
  • 冲突多久会发生一次?哪种算法最好?

按顺序散列

正如here 所解释的,Boost.Log 库维护一个全局(即静态)表,该表将名称与其哈希值相关联。可能的实现如下:

  1. 当(从字符串文字)构造名称时,会查找表(执行精确比较)。
    • 如果未找到,则在容器末尾注册。
  2. 其条目在表中的偏移量成为其哈希值。

注意事项:

  • 非常慢。 对于每个构造的名称,必须执行与注册名称一样多的字符串比较。这并不比传统的std::map 好多少,是吗?
  • 线程不安全。该表必须受到保护。
  • 在运行时强制完成。

问题

  1. 在这些条件下使用无序映射是否正确? 改用std::map 会更好吗?
  2. 如果 1 是“是”,哪种方法更好,为什么? Boost.Log 中使用的一种似乎效率很低,为什么使用它而不是我解释的另一种,即使字符串在编译时不一定知道?

注意:尽管我可以访问 gcc 和 clang 提供的实验性支持,但我还没有添加 c++14 标签。请不要犹豫,使用即将发布的规范中包含的功能。

【问题讨论】:

  • 我刚刚注意到哈希冲突只会损害性能,而不是功能(键的唯一性):多个对可能共存于同一个桶中。考虑到这一点,std::unordered_map 以及 constexpr 散列函数似乎是解决方案。

标签: c++ c++11 hash hashmap hashtable


【解决方案1】:

在这些条件下使用无序映射是否正确?会不会 改用std::map 更好吗?

虽然可能值的数量大于哈希值的类型可能会发生冲突,但当它们发生冲突时,容器会注意到哈希值标识的桶中已经有一个guest,并直接比较键。因此,不同的键永远不会发生冲突。尝试使用始终返回固定值的散列函数,看看插入键时会发生什么——它会变得,这就是散列算法很重要的原因。

因此,使用std::unordered_map 是一个不错的选择,如果正如您所提到的,会发生频繁的查找并且不需要顺序。不过,正如 D Drmmr 建议的那样,您仍然应该根据 std::map 来衡量它。

如果 1 是“是”,哪种方法更好,为什么?使用的那个 Boost.Log 好像真的效率不高,为什么用它来代替 其他我解释过,即使字符串不一定知道 编译时?

如果您担心不同的键因为它们的哈希值相等而发生冲突,那么不要害怕;如上所述,这不是问题。因此,您应该选择第一种方法,因为它允许编译时散列,并且不会遇到第二种方法的所有问题。

一种可能的实现方式:

// You stated that names were constant and constructed from string literals.
// Borrowed from the example at http://en.cppreference.com/w/cpp/language/constexpr
class
    name final
{
    private:
        const char * const
            s; // string
        const std::size_t
            l; // length

    public:
        template<std::size_t N> constexpr
            name
            ( const char (& s)[N] )
            noexcept
            : s( s ) , l( N-1 )
            { }

        // Interface that enables hashing algorithms to operate on your class.
        // If hashing is to happen at compile-time, the methods must be
        // declared `constexpr`.
};

struct
    hasher final
{
    constexpr std::size_t
        operator()
        ( const name & n )
        const noexcept
        {
            return 0; // read below
        }
};

您必须为散列算法实现一个接口才能访问您的name 类底层的数据。此外,如示例中所述,方法应为constexpr-declared;否则,它们无法从您的 constexpr 启用的散列函数中调用。至于散列算法,有很多,每种算法都适用于某些情况。 This page 详细阐述了该主题并提供了 X65599 的实现,但是,它不使用 constexpr。您可以先尝试一下,然后检查它在您的情况下的表现。

【讨论】:

    【解决方案2】:

    在这些条件下使用无序映射是否正确?会不会 改用 std::map 更好吗?

    如果您不需要对条目进行排序,使用unordered_map 通常比使用map 更有效。由于两者都有几乎相同的界面,这当然很容易测量(你应该这样做)。

    如果 1 是“是”,哪种方法是 更好,为什么? Boost.Log 中使用的那个似乎效率很低, 为什么使用它而不是我解释的另一个,即使字符串是 在编译时不一定知道?

    您应该更好地阅读 Boost 文档。我没有阅读有关线性复杂度查找的任何内容。 attribute_set 的描述建议使用关联容器(我希望 std::unordered_map,但您可以自己检查源代码)。文档中也明确提到了使用标识符而不是字符串的原因:

    "使用标识符比使用字符串效率更高。例如,复制不涉及动态内存分配,比较运算符非常轻量级。"

    这对您的情况是否有益取决于您使用这些数据结构的方式。由于您指出字符串标识符可以表示为字符串文字(但请考虑是否需要翻译这些字符串),您只需要传递一个指针即可复制字符串标识符。但是,比较仍然比boost::attribute_names 慢。

    【讨论】:

    • 在我提供的链接指向的部分中,属性名称,声明如下:“名称未作为字符串存储在 attribute_name 对象中。而是, 一个进程范围的唯一标识符被生成并与特定名称相关联。这种关联一直保留到进程终止,因此每次为相同的名称创建 attribute_name 对象时,它都会获得相同的标识符”。由此,我推断有一个全局表,并且每次创建名称时,都会查找它(表)以查看是否已经注册了相同的字符串。
    • @Kalrish 那么哪一部分是“真正低效的”?
    • @D_Drmmr 每次从字符串构造attribute_name 对象时,都会在进程范围的表中查找该字符串,即与所有先前注册的字符串进行比较。那不是效率低下吗?
    • @Kalrish 您在哪里读到使用线性复杂度查找?您是否检查了代码以亲自了解它是如何工作的?
    • @D Drmmr 虽然我不确定,但似乎是这样。请参阅svn.boost.org/svn/boost/trunk/libs/log/src/attribute_name.cpp,尤其是名称存储库单例以及与strlen 执行的比较。
    猜你喜欢
    • 2011-11-19
    • 1970-01-01
    • 2018-03-14
    • 2021-02-04
    • 1970-01-01
    • 2013-04-13
    • 1970-01-01
    • 2017-07-24
    • 2015-09-10
    相关资源
    最近更新 更多