有没有人对此有一些想法以及如何为 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_set 和std::unordered_map,查找时间可能是α*buckets + β*collisions + γ*hashtime 的函数,即查找时间随着:
- 存储桶的数量 - 存储桶越多,CPU 缓存未命中次数就越多。
- 冲突次数 - 当不同元素最终位于同一个存储桶中时。标准的容器桶是链表,因此每次碰撞都需要跟随链表到下一个元素,这是潜在的 CPU 缓存未命中;和另一个关键的比较。
- 使用哈希函数 CPU 时间。
您还可以尝试不同的哈希表,例如 skarupke::flat_hash_map 和 C++Now 2018: You Can Do Better than std::unordered_map: New Improvements to Hash Table Performance,它们不使用链接列表来解决冲突并且通常提供最佳性能。
请注意,哈希表性能很大程度上取决于键、哈希函数和大小,因此通用基准测试可能无法反映特定工作负载的性能。您需要根据您的实际工作负载/数据集对其进行基准测试。