std::set 如何比 std::map 慢?
Posted
技术标签:
【中文标题】std::set 如何比 std::map 慢?【英文标题】:How is std::set slower than std::map? 【发布时间】:2013-04-12 12:27:59 【问题描述】:我试图解决this problem from acm.timus.ru,它基本上希望我输出给定字符串的不同子字符串的数量(最大长度为 5000)。
我即将提出的解决方案效率极低,并且在限制条件下注定要做出超出时间限制的判决。但是,这两种解决方案的唯一区别(至少据我所见/理解)是一种使用std::map<long long, bool>
,而另一种使用std::set <long long>
(参见最后一个for循环的开头。其余的是相同的,您可以通过任何差异工具进行检查)。地图解决方案导致“测试 3 超过时间限制”,而设置解决方案导致“测试 2 超过时间限制”,这意味着测试 2 使得地图解决方案在其上的工作速度比设置解决方案更快。如果我选择 Microsoft Visual Studio 2010 编译器,就会出现这种情况。如果我选择 GCC,那么两种解决方案都会在测试 3 中导致 TLE。
我不是在问如何有效地解决问题。我在问什么
如何解释使用std::map
显然比使用std::set
更有效。我只是看不到这种现象的机制,希望有人能提供任何见解。
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(使用集合,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;
【问题讨论】:
你有没有自己尝试过,比如自己测量时间?甚至分析? @PlasmaHH:我相信我已经提供了足够的证据证明一个比另一个慢。我对这怎么可能很感兴趣 @PlasmaHH :我相信这是一个完全足够的问题。 @ArmenTsirunyan:除此之外,我不认为时间限制超出了来自某些黑匣子的消息,你不知道它们是如何限制时间的,它有多准确,以及其他什么影响它是比较不同程序速度的好方法,分析这两个程序将更准确地告诉您这两个程序的速度有何不同,并且很可能会告诉您足够的信息来了解自己如何做到这一点,或者让其他人向您解释这是如何可能的,通过更详细地查看时间花费在哪里。 【参考方案1】:我看到的实际差异(如果我遗漏了什么,请告诉我)是在地图案例中你所做的
hash_ans[key] = true;
在设置的情况下你这样做
hash_ans.insert(key);
在这两种情况下,都会插入一个元素,除非它已经存在,否则它什么都不做。在这两种情况下,查找都需要找到相应的元素并在失败时将其插入。实际上,在每个实现中,容器都将使用树,这使得查找同样昂贵。更何况,C++标准实际上要求set::insert()
和map::operator[]()
的复杂度是O(log n),所以两个实现的复杂度应该是一样的。
现在,一个人表现更好的原因可能是什么?一个区别是,在一种情况下,底层树的节点包含string
,而在另一种情况下,它是pair<string const, bool>
。由于pair包含一个字符串,它必须更大并且对机器的RAM接口施加更大的压力,所以这并不能解释加速。它可以做的是扩大节点大小,以便将其他节点推离缓存行,这可能对多核系统的性能不利。
总之,我会尝试一些事情:
在集合中使用相同的数据
我会使用struct data: string bool b;
来执行此操作,即将字符串捆绑在一个结构中,该结构应该具有与地图元素类似的二进制布局。作为比较器,使用less<string>
,这样只有字符串实际参与比较。
在地图上使用 insert() 我不认为这应该是一个问题,但是插入可能会导致参数的副本,即使最后没有插入也是如此。我希望它不会,所以我不太相信这会改变任何事情。
关闭调试 大多数实现都有一个诊断模式,其中迭代器被验证。您可以使用它来捕获 C++ 只说“未定义行为”的错误,耸耸肩并在您身上崩溃。这种模式通常不满足复杂性保证,并且总是有一些开销。
阅读代码 如果 set 和 map 的实现具有不同级别的质量和优化,这可以解释差异。在引擎盖下,我希望地图和场景都建立在相同类型的树上,所以这里也不抱太大希望。
【讨论】:
【参考方案2】:我猜在这种情况下,集合只比地图快一点。不过我认为你不应该考虑那么多,因为 TLE 2 或 TLE 3 并不是什么大不了的事。如果您接近给定提交的测试 2 的相同解决方案时间限制以及下一次测试 3 的时间限制,则可能会发生这种情况。我有一些解决方案仅在时间限制内通过了测试,我敢打赌,如果我重新提交它们会失败。
我使用 Ukonen 后缀树解决了这个特殊问题。
【讨论】:
这就是问题所在。设置不是更快,地图是! 我已经提交了好几次以确保 @ArmenTsirunyan 我不会在这上面浪费时间。你能做的最好的就是在你的机器上对这两种解决方案进行基准测试,看看 map 的性能是否真的比 set 好。否则最好关注实际的解决方案。【参考方案3】:这取决于使用的实现算法。通常集合是使用仅使用键字段的映射来实现的。在这种情况下,使用集合而不是地图会产生非常小的开销。
【讨论】:
我好像记得在 STLport 中,set 和 map 都是建立在同一个底层树容器之上的,所以它们的性能应该是相似的。即使没有,我也没有看到无法通过内联删除的开销,所以我现在倾向于不同意你的观点。 @doomster 我确实说过“非常轻微” :) 因为除了“地图未通过测试 2,设置未通过测试 3”之外,OP 实际上并未提及执行时间的增量,因此很难说.有了给出的信息,人们会倾向于相信 GCC 实现使用相同的算法。正如我在回答中(含蓄地)所说,Microsft 可能使用不同的实现。以上是关于std::set 如何比 std::map 慢?的主要内容,如果未能解决你的问题,请参考以下文章