我有一个用例,其中将搜索一组字符串以查找特定字符串s
。这些搜索的匹配百分比或正匹配将非常高。假设99%以上的时间,s
将出现在集合中。
我现在正在使用boost::unordered_set
,即使使用非常快速哈希算法,良好硬件上 40ms 600ms < / s>一个VM来搜索该组500,000次。是的,这是非常好的,但我正在努力的事情是不可接受的。
那么,是否存在针对高命中率进行优化的任何类型的数据结构?我无法预先计算出来的字符串的哈希值,因此我认为我正在查看类似boost::unordered_set
的哈希集的\ $ O(平均字符串长度)\ $的复杂性。我看了一下Tries,这些可能在相反的情况下表现很好,但是很少有点击,但实际上并不比哈希集更好。
编辑:我的特定用例的其他一些细节:
集合中的字符串数量约为5,000。最长的字符串可能不超过200个字符。使用相同的字符串一次又一次地调用搜索,但它们是从外部系统进入的,我无法预测下一个字符串将是什么。确切的匹配率实际上是99.975%。
edit2 :我做了一些自己的基准测试
我收集了5000个真实系统中出现的字符串。我创建了两个场景。
1)我遍历已知字符串列表并在容器中搜索它们。我为500,000次搜索(“点击”)执行此操作。
2)我循环查看一组已知不在容器中的字符串,进行500,000次搜索(“未命中”)。
(注意 - 我对反向散列数据很感兴趣,因为我的数据很有用,我注意到有很多常见的前缀和后缀不同 - 至少就是它的样子。)
在macbook主机上运行的虚拟机CentOS 5.6 VM上进行测试。
hits (ms) misses (ms)
boost::unordered_set with default hash and no reserved size: 591.15 441.39
tr1::unordered_set with default hash 191.09 143.80
boost::unordered_set with a reserve size set: 579.31 431.54
boost::unordered_set w/custom hash (hash on the last 15 chars + str size): 357.34 812.13
boost::unordered_set w/custom hash (hash on the last 25 chars + str size): 362.60 795.33
trie: 1809.34 58.11
trie with reversed insertion/search: 2806.26 311.14
在我的测试中,有很多匹配,tr1设置是最好的。在有很多失误的地方,特里赢了很多。
我的测试循环看起来像这样,其中function_set是被测试的容器,加载了5,000个字符串,函数是容器中所有字符串或一堆不在容器中的字符串的向量。
while (searched < kTotalSearches) {
for(std::vector<std::string>::const_iterator i = functions.begin(); i != functions.end(); ++i) {
function_set.count(*i);
searched++;
if (searched == kTotalSearches)
break;
}
}
std::cout << searched << " searches." << std::endl;
答案 0 :(得分:3)
我很确定Tries是您正在寻找的。保证不要使用大于字符串长度的节点数。到达叶子后,如果此特定节点存在冲突,则可能会进行一些线性搜索。这取决于你如何构建它。既然你正在使用一套我会认为这不是问题。
unordered_set的复杂度将更差O(n),但在这种情况下,n是您拥有的节点数(500k),而不是您要搜索的字符数(可能小于500k)。
编辑后: 也许你真正需要的是在你的搜索算法成功后缓存结果。
答案 1 :(得分:2)
如果字符串集在编译时是固定的(例如,它是已知人类单词的字典),您可以使用perfect hash算法,并使用gperf生成器。
否则,你可能会使用一个由26个哈希表组成的数组,用该字的第一个字母索引来哈希。
BTW,也许使用具有二分访问的这些字符串的排序数组可能更快(因为log 5000约为13),或者std::map
或std::set
。
最后,您可以定义自己的散列函数:也许在您的特定情况下,只散列前16个字节就足够了!
如果字符串集是固定的,您可以考虑在其上生成二分搜索(例如,编写脚本以生成具有5000个测试的函数,但仅执行log 5000)。
此外,即使字符串集稍有变量(例如从一个程序运行更改为下一个程序,但在单次运行期间保持不变),您甚至可以考虑生成函数(通过发出C ++代码,然后编译它) )在飞行中dlopen
- 它。
你真的应该测试并试用几种解决方案!它可能更像是一个工程问题,而不是算法问题。
答案 2 :(得分:2)
这个问题激起了我的好奇心,所以我做了一些测试来满足自己以下结果。一些一般性说明:
测试的基本描述与相关细节一起运行:
std::unordered_map<std::string, int>
std::unordered_set<std::string>
boost::unordered_set<std::string>
,v1.47.0 std::unordered_map<const char*, int>
std::unordered_set<const char*>
std::unordered_map<>
使用自定义FNV-1a哈希算法。std::unordered_set<>
使用自定义FNV-1a哈希算法。std::binary_search()
上使用std::vector<std::string>
。size_t VectorIndex[26][26][26][26][26]
数组索引到已排序的数组。std::unordered_map<>
使用来自gperf的完美哈希。is_word_set()
功能。从最慢到最快排序的结果(对于大多数点击的情况,~99.975%):
从最慢到最快排序的结果(对于大多数未命中的情况,约0.1%命中):
我的第一个猜测是,trie非常适合这类事情,但从结果来看,实际情况似乎是正确的。考虑一下这一点是有道理的,并且与not use a linked-list的原因相同。
我假设您可能熟悉每个程序员应该知道的table of latencies。在你的情况下,你有40万次查找在40ms或80ns /查找执行。如果您必须访问L1 / L2缓存中尚未存在的任何内容,那么您将很容易丢失。由于你对每个角色都有一个间接的,可能是非本地的内存访问,因此trie非常糟糕。鉴于在这种情况下trie的大小,我无法想象任何方式让整个trie适应缓存以提高性能(尽管可能)。我仍然认为,即使你确实让trie完全适合L2缓存,你也会因为所需的所有间接而失败。
std::unordered_
容器实际上可以很好地完成开箱即用的工作。实际上,在尝试加速它们时,我实际上使它们变慢(在命名不佳的FastMap和FastSet试验中)。
尝试从std::string
切换到const char *
(大约慢两倍)也是一样。
boost::unordered_set<>
的速度是std::unordered_set<>
的两倍,我不知道是不是因为我刚使用了内置的哈希函数,而是使用稍微老一点的版本, 或者是其他东西。你自己试过std::unordered_set<>
吗?
通过使用gperf
,如果您的字符串集在编译时已知,则可以轻松创建完美的哈希函数。您可以在运行时创建一个完美的哈希值,具体取决于新字符串添加到地图的频率。与标准地图实施相比,这可以使您的速度提高23%。
PerfectWordSetThread 测试只需使用完美哈希并将工作分成1-32个线程。这个问题是完全平行的(至少是基准测试),因此在16线程的情况下,你的性能几乎提高了5倍。这仅适用于6.3ms / 500k查询,或13 ns /查找...在4GHz处理器上仅需50个周期。
StringIteration 案例确实指出了加快速度会有多困难。只需迭代正在找到的字符串需要350毫秒,或70%的时间与500毫秒映射的情况相比。即使您可以完美地猜测每个字符串,您仍然需要这350毫秒(对于1000万次查找)来实际比较并验证匹配。
编辑:另一件说明事情紧张的事情是4050毫秒的 PerfectWordSetFunc 和500毫秒的 PerfectWordSet 之间的区别。两者之间的唯一区别是一个在函数中被调用,一个被称为内联。将其称为函数会将速度降低8倍。在基本伪代码中,这只是:
bool IsInPerfectWordSet (string Match)
{
return in_word_set(Match);
}
//Inline benchmark: PerfectWordSet
for i = 1 to 10,000,000
{
if (in_word_set(SomeString)) ++MatchCount;
}
//Function call benchmark: PerfectWordSetFunc
for i = 1 to 10,000,000
{
if (IsInPerfectWordSet(SomeString)) ++MatchCount;
}
这真正突出了内联代码/功能可以带来的性能差异。您还必须小心确保在基准测试中测量的是什么。有时您会希望包含函数调用开销,有时则不包括。
我学会了永远不会说&#34;没有&#34;对于这个问题,但在某些时候,努力可能不值得。如果您可以将查找拆分为线程并使用完美或接近完美的哈希函数,那么您应该能够每秒接近1亿次查找匹配(在具有多个物理处理器的计算机上可能更多)。
有些想法我不具备尝试的知识: