是否存在针对搜索优化的类似集合的数据结构,其中提前知道匹配的百分比很高?

时间:2014-10-31 00:37:59

标签: c++ data-structures abstract-data-type

我有一个用例,其中将搜索一组字符串以查找特定字符串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;

3 个答案:

答案 0 :(得分:3)

我很确定Tries是您正在寻找的。保证不要使用大于字符串长度的节点数。到达叶子后,如果此特定节点存在冲突,则可能会进行一些线性搜索。这取决于你如何构建它。既然你正在使用一套我会认为这不是问题。

unordered_set的复杂度将更差O(n),但在这种情况下,n是您拥有的节点数(500k),而不是您要搜索的字符数(可能小于500k)。

编辑后: 也许你真正需要的是在你的搜索算法成功后缓存结果。

答案 1 :(得分:2)

如果字符串集在编译时是固定的(例如,它是已知人类单词的字典),您可以使用perfect hash算法,并使用gperf生成器。

否则,你可能会使用一个由26个哈希表组成的数组,用该字的第一个字母索引来哈希。

BTW,也许使用具有二分访问的这些字符串的排序数组可能更快(因为log 5000约为13),或者std::mapstd::set。 最后,您可以定义自己的散列函数:也许在您的特定情况下,只散列前16个字节就足够了!

如果字符串集是固定的,您可以考虑在其上生成二分搜索(例如,编写脚本以生成具有5000个测试的函数,但仅执行log 5000)。

此外,即使字符串集稍有变量(例如从一个程序运行更改为下一个程序,但在单次运行期间保持不变),您甚至可以考虑生成函数(通过发出C ++代码,然后编译它) )在飞行中dlopen - 它。

你真的应该测试并试用几种解决方案!它可能更像是一个工程问题,而不是算法问题。

答案 2 :(得分:2)

这个问题激起了我的好奇心,所以我做了一些测试来满足自己以下结果。一些一般性说明:

  • 关于基准测试的常见警告适用(不要相信我的数字,根据您的具体用例和数据做自己的基准测试......)。
  • 使用MSVS C ++ 2010(速度优化,发布版本)完成测试。
  • 使用1000万个循环运行基准测试以提高计时准确性。
  • 通过将20个不同的字符串片段随机连接成长度为4到65个字符的字符串来生成名称。
  • 名称仅包含字母,而某些测试(trie)为了简单起见不区分大小写,但没有理由不将这些方法扩展为包含其他字符。
  • 测试尝试匹配问题中给出的99.975%命中率。

测试说明

测试的基本描述与相关细节一起运行:

  • 字符串迭代 - 只需遍历函数名称进行基线时间比较。
  • 地图 - std::unordered_map<std::string, int>
  • 设置 - std::unordered_set<std::string>
  • BoostSet - boost::unordered_set<std::string>,v1.47.0
  • CharMap - std::unordered_map<const char*, int>
  • CharSet - std::unordered_set<const char*>
  • FastMap - 只需std::unordered_map<>使用自定义FNV-1a哈希算法。
  • FastSet - 只需std::unordered_set<>使用自定义FNV-1a哈希算法。
  • CustomMap - 我多年前写的一个基本哈希映射。
  • Trie - 从Google code下载的标准特里。
  • CustomTrie - 我自己写的一个简单的特里。
  • 二进制搜索 - 在已排序的std::binary_search()上使用std::vector<std::string>
  • SortArrayMap - 尝试使用size_t VectorIndex[26][26][26][26][26]数组索引到已排序的数组。
  • PerfectMap - std::unordered_map<>使用来自gperf的完美哈希。
  • PerfectWordSet - 直接使用gperf is_word_set()功能。
  • PerfectWordSetFunc - 与 PerfectWordSet 相同,但在函数中调用而不是内联。
  • PerfectWordSetThread - 与 PerfectWordSet 相同,但工作分为N个线程(标准窗口线程)。除等待线程完成外,不使用同步。

测试结果(主要是命中)

从最慢到最快排序的结果(对于大多数点击的情况,~99.975%):

  • Trie - 9100 ms
  • SortArrayMap - 6600毫秒
  • PerfectWordSetFunc - 4050 ms
  • CustomTrie - 3470 ms
  • 二进制搜索 - 3420 ms
  • CustomMap - 2700毫秒
  • CharSet - 1300毫秒
  • CharMap - 1300毫秒
  • BoostSet - 1200毫秒
  • FastSet - 970 ms
  • FastMap - 930 ms
  • 原创海报 - 800毫秒(估计)
  • 设置 - 730毫秒
  • 地图 - 690毫秒
  • PerfectMap - 650 ms
  • PerfectWordSet - 500毫秒
  • PerfectWordSetThread(1) - 500 ms
  • StringIteration - 350毫秒
  • PerfectWordSetThread(2) - 260 ms
  • PerfectWordSetThread(4) - 150毫秒
  • PerfectWordSetThread(32) - 125 ms
  • PerfectWordSetThread(8) - 120 ms
  • PerfectWordSetThread(16) - 110 ms

测试结果(大多数未命中)

从最慢到最快排序的结果(对于大多数未命中的情况,约0.1%命中):

  • 二进制搜索 - ? (花了太长时间)
  • SortArrayMap - 8050 ms
  • Trie - 3200 ms
  • CustomMap - 1700 ms
  • BoostSet - 920 ms
  • CustomTrie - 850 ms
  • FastMap - 590 ms
  • FastSet - 580 ms
  • CharSet - 550毫秒
  • CharMap - 550 ms
  • StringIteration - 350毫秒
  • 设置 - 330毫秒
  • 地图 - 330毫秒
  • PerfectMap - 280 ms
  • PerfectWordSet - 140毫秒
  • PerfectWordSetThread(1) - 130 ms
  • PerfectWordSetThread(2) - 75 ms
  • PerfectWordSetThread(4) - 45 ms
  • PerfectWordSetThread(32) - 45 ms
  • PerfectWordSetThread(8) - 40 ms
  • PerfectWordSetThread(16) - 35 ms

讨论

我的第一个猜测是,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亿次查找匹配(在具有多个物理处理器的计算机上可能更多)。

有些想法我不具备尝试的知识:

  1. 使用SSE进行装配优化
  2. 使用GPU获得额外的吞吐量
  3. 更改您的设计,以便您不需要快速查找
  4. 花点时间考虑#3 ....最快的代码是永远不需要运行的代码。如果您可以减少查找次数或减少对极高吞吐量的需求,那么您就不需要花时间微优化最终查找功能。