【问题标题】:how to find/implement a good hash function for bitset如何为 bitset 找到/实现一个好的散列函数
【发布时间】:2020-06-12 10:22:59
【问题描述】:

我正在尝试使用 unordered_map,其中密钥(存储在 bitset 中)由 Morton 编码生成。我测试了几种情况,密钥在 2^0-2^6、2^3-2^9(最后 3 位为零)、2^6-2^12(最后 6 位为零)范围内) 和 2^9-2^15(最后 9 位为零)。

当我使用 Visual Studio 提供的 bitset 的默认哈希函数时,一切似乎都很好。对于所有情况,在 unordered_map 中查找元素的时间是相同的,并且随着容器的大小单调增加。

当我试图减少查找元素的时间时,出现了问题。我使用哈希函数

class my_bitset_hash
{
public:
    size_t operator()(const bitset<64>& key) const
    {
        size_t hashVal = 0;
        hashVal = key.to_ullong();
        return hashVal;
    }
};

key 在 2^9-2^15 范围内查找元素的时间至少比 key 在 2^0-2^6 范围内大两个数量级,即使 unorder_map 的大小相同。据我所知,如果没有碰撞,查找的时间消耗应该是最低的,时间复杂度应该是 O(1)。此外,与使用默认功能的案例相比,所有案例都需要更多时间查找。

有没有人对此有一些想法以及如何为 bitset 找到一个好的散列函数?

谢谢

【问题讨论】:

  • 这似乎是在这样的差异很容易只是噪音的时间范围内。您是否对足够大的输入进行了分析?您的机器是否在所有测试运行中都具有相同的负载(在后台运行浏览器或游戏可能会发生很大变化)。您是否使用分析器来找出这种差异的确切位置? (也许 to_ullong() 对于非常短的位集来说更快......有很多可能性)
  • 根据some sources Visual Studio 的 unordered_map 的实现只取了哈希值的低位,所以结果很明显。要么切换到另一个哈希表,要么切换到另一个哈希映射实现。
  • 假设您的“在 2^9-2^15 范围内”的键是(大部分)全等模 2^(一些小数字)。
  • 您可以通过将hashVal &lt;&lt;= 32; 添加到您的哈希函数来测试这一点。如果@user202729 所说的对你来说是这样,这应该会影响你的两个测试范围的性能。
  • 谢谢大家。我认为是这种情况,因为输入是基于

标签: c++ hashtable


【解决方案1】:

有没有人对此有一些想法以及如何为 bitset 找到一个好的散列函数?

尝试不同的哈希函数。

如果您的字典在使用运行时数据创建后仍然不可变,请尝试暴力破解哈希函数种子。

一个尽量减少冲突的例子:

#include <unordered_set>
#include <iostream>
#include <cmath>

struct Stats {
    static int constexpr BINS = 8;
    size_t size = 0;
    size_t buckets = 0;
    double load_pct = 0;
    double collision_pct = 0;
    unsigned collisions[BINS] = {};
};

std::ostream& operator<<(std::ostream& o, Stats const s) {
    o << "size: " << s.size << ", ";
    o << "buckets: " << s.buckets << ", ";
    o << "load: " << std::round(s.load_pct) << "%, ";
    o << "collisions: " << std::round(s.collision_pct) << "% [";
    for(auto const& bin : s.collisions)
        o << bin << ',';
    return o << "]\n";
}

template<class C>
Stats stats(C const& c) {
    Stats s;
    s.size = c.size();
    s.buckets = c.bucket_count();
    s.load_pct = 100. * c.size() / c.bucket_count();

    size_t collisions = 0;
    for(auto bucket_idx = c.bucket_count(); bucket_idx--;) {
        auto elements_in_bucket = std::distance(c.begin(bucket_idx), c.end(bucket_idx));
        if(elements_in_bucket > 1) {
            ++collisions;
            ++s.collisions[std::min<unsigned>(Stats::BINS - 1, elements_in_bucket - 2)];
        }
    }
    s.collision_pct = 100. * collisions / c.size();

    return s;
}

// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
struct Fnv1a32 {
    static constexpr unsigned BASIS = 16777619;
    static constexpr unsigned PRIME = 2166136261;

    unsigned const state_;

    static constexpr unsigned hash(void const* key, size_t len, unsigned state) noexcept {
        unsigned char const* p = static_cast<unsigned char const*>(key);
        unsigned char const* const e = p + len;
        for(; p < e; ++p)
            state = (state ^ *p) * PRIME;
        return state;
    }

public:
    constexpr Fnv1a32(unsigned seed = 0)
        : state_(seed ? hash(&seed, sizeof seed, BASIS) : BASIS)
    {}

    template<class T>
    std::enable_if_t<std::is_integral<T>::value, unsigned> operator()(T key) const {
        return hash(&key, sizeof key, state_);
    }
};

int main() {
    // Using std::hash.
    std::unordered_set<unsigned> s;
    for(unsigned n = 1000; n--;)
        s.insert(static_cast<unsigned>(std::rand()) << 6);
    std::cout << "std::hash:    " << stats(s);

    // Using Fnv1a32.
    std::unordered_set<unsigned, Fnv1a32> s2(s.bucket_count());
    s2.insert(s.begin(), s.end());
    std::cout << "Fnv1a32:      " << stats(s2);

    // Brute-force Fnv1a32 seed.
    double best_collision_pct = std::numeric_limits<double>::infinity();
    unsigned best_seed = 0;
    for(unsigned seed = 0; seed < 10000; ++seed) {
        std::unordered_set<unsigned, Fnv1a32> s3(s2.bucket_count(), Fnv1a32{seed});
        s3.insert(s2.begin(), s2.end());
        auto const stats3 = stats(s3);
        if(stats3.collision_pct < best_collision_pct) {
            best_collision_pct = stats3.collision_pct;
            best_seed = seed;
        }
    }

    // Using Fnv1a32 with the best seed.
    std::unordered_set<unsigned, Fnv1a32> s4(s2.bucket_count(), Fnv1a32{best_seed});
    s4.insert(s2.begin(), s2.end());
    std::cout << "Fnv1a32 best: "  << stats(s4);
}

输出:

std::hash:    size: 1000, buckets: 1493, load: 67%, collisions: 21% [152,46,6,1,0,0,0,0,]
Fnv1a32:      size: 1000, buckets: 1613, load: 62%, collisions: 21% [177,29,3,1,0,0,0,0,]
Fnv1a32 best: size: 1000, buckets: 1741, load: 57%, collisions: 17% [135,24,5,1,0,0,0,0,]

您可能希望以类似方式最小化的另一个指标是查找时间。

对于std::unordered_setstd::unordered_map,查找时间可能是α*buckets + β*collisions + γ*hashtime 的函数,即查找时间随着:

  • 存储桶的数量 - 存储桶越多,CPU 缓存未命中次数就越多。
  • 冲突次数 - 当不同元素最终位于同一个存储桶中时。标准的容器桶是链表,因此每次碰撞都需要跟随链表到下一个元素,这是潜在的 CPU 缓存未命中;和另一个关键的比较。
  • 使用哈希函数 CPU 时间。

您还可以尝试不同的哈希表,例如 skarupke::flat_hash_mapC++Now 2018: You Can Do Better than std::unordered_map: New Improvements to Hash Table Performance,它们不使用链接列表来解决冲突并且通常提供最佳性能。

请注意,哈希表性能很大程度上取决于键、哈希函数和大小,因此通用基准测试可能无法反映特定工作负载的性能。您需要根据您的实际工作负载/数据集对其进行基准测试。

【讨论】:

  • 感谢您的回复。我已经测试了元素为 1.5m、4.0m 和 8.3m 的 unordered_map 的几个散列函数(即 Knuth 乘法、除法、中方、FVN-1、FVN-1a、MurmurHash、CityHash)。当 hashVal = key 时,查找时间最短。所有情况的内存使用差异在 5% 以内,相同数量的元素的负载相同 (75% - 98%)。需要注意的是,当hashVal=key的时候,碰撞的概率是3%到25%(应该是0%吧?)。我认为我最好编写自己的哈希映射,而不是使用 Visual Studio 提供的默认哈希映射。
  • @Zhengliang 在这里找到您后续问题的答案和更多信息:bigdata.uni-saarland.de/publications/p249-richter.pdf 另外,在实现您自己的哈希表之前研究我发布的那些链接,它们非常有见地。
猜你喜欢
  • 1970-01-01
  • 2013-12-28
  • 2011-10-06
  • 2016-06-28
  • 2021-02-15
  • 1970-01-01
  • 1970-01-01
  • 2011-04-29
相关资源
最近更新 更多