简体   繁体   English

std :: set如何比std :: map慢?

[英]How is std::set slower than std::map?

I was trying to solve this problem from acm.timus.ru which basically wants me to output the number of different substrings of a given string (max length 5000). 我试图从acm.timus.ru解决这个问题,它基本上要我输出给定字符串的不同子串的数量(最大长度5000)。

The solutions I am about to present are desperately inefficient and doomed for Time Limit Exceeded verdict given the constraints. 我即将呈现的解决方案极其低效,并且考虑到约束条件,注定会超时限制超过判决。 However, the only way in which these two solutions differ (at least as far as I can see/understand) is that one uses std::map<long long, bool> , while the other uses std::set <long long> (see the beginning of the last for loop. The rest is identical, you can check by any diff tool). 但是,这两种解决方案不同的唯一方法(至少据我所知/理解)是一个使用std::map<long long, bool> ,而另一个使用std::set <long long> (参见最后一个for循环的开头。其余的是相同的,你可以通过任何diff工具检查)。 The map solution results in "Time Limit Exceeded on Test 3", whereas the set solution results in "Time Limit Exceeded on Test 2", which means that Test 2 is such that the map solution works faster on it than the set solution. 映射解决方案导致“测试3超出时间限制”,而设置解决方案导致“测试2超出时间限制”,这意味着测试2使得映射解决方案比设置解决方案更快地工作。 This is the case if I choose Microsoft Visual Studio 2010 compiler. 如果我选择Microsoft Visual Studio 2010编译器就是这种情况。 If I choose GCC, then both solutions result in TLE on test 3. 如果我选择GCC,则两种解决方案都会在测试3中产生TLE。

I am not asking for how to solve the problem efficiently. 我不是要求如何有效地解决问题。 What I am asking is how can one explain that using std::map can apparently be more efficient than using std::set . 我要问的是如何解释使用std::map显然比使用std::set更有效。 I just fail to see the mechanics of this phenomenon and hope that someone could have any insights. 我只是没有看到这种现象的机制,并希望有人可以有任何见解。

Code1 (uses map, TLE 3): Code1(使用地图,TLE 3):

#include <iostream>
#include <map>
#include <string>
#include <vector>

using namespace std;

int main ()
{
   string s;
   cin >> s;
   vector <long long> p;
   p.push_back(1);
   for (int i = 1; i < s.size(); i++)
      p.push_back(31 * p[i - 1]);
   vector <long long> hash_temp;
   hash_temp.push_back((s[0] - 'a' + 1) * p[0]);
   for (int i = 1; i < s.size(); i++)
      hash_temp.push_back((s[i] - 'a' + 1) * p[i] + hash_temp[i - 1]);   
   int n = s.size();   
   int answer = 0;
   for (int i = 1; i <= n; i++)
   {
      map <long long, bool> hash_ans;
      for (int j = 0; j < n - i + 1; j++)
      {
         if (j == 0)
            hash_ans[hash_temp[j + i - 1] * p[n - j - 1]] = true;
         else
            hash_ans[(hash_temp[j + i - 1] - hash_temp[j - 1]) * p[n - j - 1]] = true;
      }
      answer += hash_ans.size();
   }
   cout << answer;
}

Code2 (uses set, TLE 2): Code2(使用set,TLE 2):

#include <iostream>
#include <string>
#include <vector>
#include <set>

using namespace std;

int main ()
{
   string s;
   cin >> s;
   vector <long long> p;
   p.push_back(1);
   for (int i = 1; i < s.size(); i++)
      p.push_back(31 * p[i - 1]);
   vector <long long> hash_temp;
   hash_temp.push_back((s[0] - 'a' + 1) * p[0]);
   for (int i = 1; i < s.size(); i++)
      hash_temp.push_back((s[i] - 'a' + 1) * p[i] + hash_temp[i - 1]);   
   int n = s.size();   
   int answer = 0;
   for (int i = 1; i <= n; i++)
   {
      set <long long> hash_ans;
      for (int j = 0; j < n - i + 1; j++)
      {
         if (j == 0)
            hash_ans.insert(hash_temp[j + i - 1] * p[n - j - 1]);
         else
            hash_ans.insert((hash_temp[j + i - 1] - hash_temp[j - 1]) * p[n - j - 1]);
      }
      answer += hash_ans.size();
   }
   cout << answer;
}

The actual differences I see (tell me if I missed anything) are that in the map case you do 我看到的实际差异(告诉我,如果我错过了什么)是你在地图中的情况

hash_ans[key] = true;

while in the set case you do 而在你做的情况下

hash_ans.insert(key);

In both cases, an element is inserted, unless it already exists, in which it does nothing. 在这两种情况下,都会插入一个元素,除非它已经存在,否则它不会执行任何操作。 In both cases, the lookup needs to locate the according element and insert it on failure. 在这两种情况下,查找都需要找到相应的元素并在失败时插入它。 In effectively every implementation out there, the containers will use a tree, making the lookup equally expensive. 在有效的每个实现中,容器将使用树,使查找同样昂贵。 Even more, the C++ standard actually requires set::insert() and map::operator[]() to be O(log n) in complexity, so the complexity of both implementations should be the same. 更重要的是,C ++标准实际上要求set::insert()map::operator[]()复杂度为O(log n),因此两种实现的复杂性应该相同。

Now, what could be the reason that one performs better? 现在,一个人表现得更好的原因是什么? One difference is that in one case a node of the underlying tree contains a string , while in the other it's a pair<string const, bool> . 一个区别是,在一种情况下,底层树的节点包含一个string ,而在另一种情况下,它是一pair<string const, bool> Since the pair contains a string, it must be larger and put more pressure on the RAM interface of the machine, so this doesn't explain the speedup. 由于该对包含一个字符串,它必须更大并且对机器的RAM接口施加更大的压力,因此这并不能解释加速。 What it could do is enlarge the node size so that other nodes are pushed off the cache line, which can be bad for performance in multi-core system. 它可以做的是扩大节点大小,以便其他节点被推离缓存线,这可能对多核系统的性能有害。

In summary, there's some things I'd try: 总之,我尝试过一些事情:

  1. use the same data in the set 在集合中使用相同的数据
    I'd do this with struct data: string {bool b}; 我用struct data: string {bool b};做这个struct data: string {bool b}; ie bundle the string in a struct that should have a similar binary layout as the map's elements. 即将字符串捆绑在一个结构中,该结构应具有与地图元素类似的二进制布局。 As comparator, use less<string> , so that only the string actually takes part in the comparisons. 作为比较器,使用less<string> ,以便只有字符串实际参与比较。

  2. use insert() on the map 在地图上使用insert()
    I don't think this should be an issue, but the insert could incur a copy of the argument, even if no insert takes place in the end. 我不认为这应该是一个问题,但插入可能会产生一个参数的副本,即使最后没有插入。 I would hope that it doesn't though, so I'm not too confident this will change anything. 我希望它不会,所以我不太自信这将改变任何事情。

  3. turn off debugging 关闭调试
    Most implementations have a diagnostic mode, where iterators are validated. 大多数实现都具有诊断模式,其中验证了迭代器。 You can use this to catch errors where C++ only says "undefined behaviour", shrugs its shoulders and crashes on you. 您可以使用它来捕获C ++仅表示“未定义的行为”的错误,耸耸肩并且崩溃。 This mode often doesn't meet complexity guarantees and it always has some overhead. 此模式通常不符合复杂性保证,并且总是有一些开销。

  4. read the code 阅读代码
    If the implementations for set and map have different levels of quality and optimization, this could explain the differences. 如果集合和映射的实现具有不同的质量和优化级别,则可以解释这些差异。 Under the hood, I'd expect both map and set to be built on the same type of tree though, so not much hope here either. 在引擎盖下,我希望map和set都可以构建在相同类型的树上,所以这里也没有太多希望。

A set is only a little bit faster than a map in this case I guess. 在我猜这种情况下,一组只比地图快一点。 Still I don't think you should case that much as TLE 2 or TLE 3 is not really a big deal. 我仍然不认为你应该这么做,因为TLE 2或TLE 3并不是什么大不了的事。 It may happen if you are clsoe to the time limit that the same solution time limits on test 2 on a given submit and next time it time limits on test 3. I have some solutions passing the tests just on the time limit and I bet if I resubmit them they will fail. 如果您在给定提交上的测试2上的相同解决方案时间限制和下次测试3的时间限制时,可能会发生这种情况。我有一些解决方案仅在时间限制上通过测试我打赌如果我重新提交他们会失败。

This particular problem I solved using a Ukonen Sufix tree. 我使用Ukonen Sufix树解决了这个特殊问题。

It depends on the implementation algorithms used. 这取决于所使用的实现算法。 Usually sets are implemented using maps only using the key field. 通常只使用关键字段使用地图实现集合。 In such case there would be a very slight overhead for using a set as opposed to a map. 在这种情况下,使用集合而不是映射会有非常小的开销。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM