C++ &引用;“扁平化”;标准:设置<;标准::字符串>;用于存储和比较?
这可能是一个愚蠢的问题,因为std::set已经有了非常好的比较运算符,但我认为我可能会针对我的特定用例进行优化,并希望确保我不会以某种方式伤害自己 本质上,我有一个代价高昂的操作,它将std::set&作为输入。我正在缓存操作的结果,这样如果已经传入了相同的输入,我就可以返回结果。这确实需要存储集合的副本(我在C++ &引用;“扁平化”;标准:设置<;标准::字符串>;用于存储和比较?,c++,string,performance,stl,set,C++,String,Performance,Stl,Set,这可能是一个愚蠢的问题,因为std::set已经有了非常好的比较运算符,但我认为我可能会针对我的特定用例进行优化,并希望确保我不会以某种方式伤害自己 本质上,我有一个代价高昂的操作,它将std::set&作为输入。我正在缓存操作的结果,这样如果已经传入了相同的输入,我就可以返回结果。这确实需要存储集合的副本(我在 std::map<std::set<std::string>, Result*> std::map ,然后每次调用该操作时都进行搜索。由于同一操作很可能会连
std::map<std::set<std::string>, Result*>
std::map
,然后每次调用该操作时都进行搜索。由于同一操作很可能会连续被调用数千次,因此我认为缓存的std::set在99%以上的时间内都会被找到。最近,我尝试了一个我认为可能是一个小的改进,基于某些字符是传入字符串中的nvalid:我将std::set展平为单个字符串,组件字符串用“:”字符分隔。然后,我的std::map变为
std::map<std::string, Result*>
std::map
每次调用该操作时,集合都被展平,并在缓存中搜索单个字符串
实际上,我对性能的提高感到惊讶。我的测试运行使用了包含5个字符串的std::set,每个字符串长度为30个字符,并且运行了10000000次搜索。在我的工作站上,每次运行的时间是
std::map<std::set<std::string>, Result*> : 138.8 seconds
std::map<std::string, Result> : 89.2 seconds
std::map:138.8秒
标准:映射:89.2秒
看起来,即使每次调用都要将集合平坦化,第二种方法也是一个巨大的改进。我想我的问题是:为什么?我在这里做了一些std::set的实现者故意避免的潜在的坏事(即,可能导致较大字符串的坏堆碎片?)这仅仅是因为场景中的单个字符串位于不同的位置,并且必须分别进行比较吗?我是在攻击自己吗?在这种特定情况下,改进似乎太明显了,无法提高性能
为什么?
数据位置
通常是作为二进制搜索树来实现的,它可能是因为在您的机器上用“代码> STS::String 缓存,与
set
功能的包装器),这可能需要大量的工作。但在大多数情况下,这是不必要的;大多数典型的代码只需要几个函数就可以看到,通常只有两三个
#include <iostream>
#include <set>
#include <utility>
template <class T>
class tracked_set {
std::set<T> data;
size_t version = 0;
public:
typedef typename std::set<T>::iterator iterator;
std::pair<iterator, bool> insert(T &&d) {
auto ret = data.insert(std::forward<T>(d));
version += ret.second;
return ret;
}
iterator erase(iterator i) {
auto ret = data.erase(i);
if (ret != data.end())
++version;
}
// At least if memory serves, even non-const iterators on a `set` don't
// allow the set to be modified, so these should be safe.
auto begin() { return data.begin(); }
auto end() { return data.end(); }
auto rbegin() { return data.rbegin(); }
auto rend() { return data.rend(); }
// The `c*` iterator functions return const_iterator's, so
// they're definitely safe.
auto cbegin() const { return data.cbegin(); }
auto cend() const { return data.cend(); }
auto crbegin() const { return data.crbegin(); }
auto crend() const { return data.crend(); }
class token {
std::set<T> const *addr;
size_t version;
public:
friend bool operator==(token const &a, token const &b) {
return a.addr == b.addr && a.version == b.version;
}
token(tracked_set const &ts) {
addr = &ts.data;
version = ts.version;
}
};
operator token() const { return token(*this); }
};
int main() {
using T = tracked_set<int>;
T ts;
ts.insert(1);
ts.insert(2);
T::token t(ts);
if (t == T::token(ts))
std::cout << "Good\n";
ts.insert(3);
if (t == T::token(ts))
std::cout << "bad\n";
}
#包括
#包括
#包括
模板
类集{
std::set数据;
大小\u t版本=0;
公众:
typedef typename std::set::迭代器迭代器;
标准:成对插入(T&d){
auto-ret=data.insert(std::forward(d));
版本+=ret.second;
返回ret;
}
迭代器擦除(迭代器i){
自动恢复=数据擦除(i);
if(ret!=data.end())
++版本;
}
//至少在内存可用的情况下,即使“set”上的非常量迭代器也不行
//允许修改集合,这样应该是安全的。
自动开始(){返回数据。开始();}
自动结束(){返回数据。结束();}
自动rbegin(){返回数据。rbegin();}
自动rend(){return data.rend();}
//`c*`迭代器函数返回常量迭代器,因此
//他们绝对安全。
auto cbegin()常量{return data.cbegin();}
auto cend()常量{return data.cend();}
自动crbegin()常量{返回数据.crbegin();}
auto crend()常量{return data.crend();}
类令牌{
std::set const*addr;
尺寸(t)版本;;
公众:
友元布尔运算符==(令牌常量和a、令牌常量和b){
返回a.addr==b.addr&&a.version==b.version;
}
令牌(被跟踪的集合常量(&T){
addr=&ts.data;
version=ts.version;
}
};
运算符标记()常量{返回标记(*this);}
};
int main(){
使用T=跟踪的_集;
T-ts;
ts.插入(1);
ts.插入(2);
T::令牌T(ts);
if(t==t::token(ts))
std::cout如果你99%的时间都用相同的参数调用函数,那么我会说调用者有问题,而不是函数本身。无论如何,你不能在你的集合中添加某种id
,这样方法只需要比较该id
,而不是整个set
您传递的e集不会经常更改。我确实简化了一点,函数的输入是std::set和两条单独的要比较的消息。该集合描述了在比较之前应用于消息的转换,而构建此转换是成本高昂的部分(应用它很简单)集合几乎总是不变的,但消息几乎总是不同的。理想情况下,我会让调用方以某种方式获取转换句柄,然后在调用比较时使用句柄而不是集合-不幸的是,这需要替换现有代码。请确保不能使用分隔符这是实际字符串的一部分,您应该很好。此外,无论何时性能都不要忘记使用std::unordered_映射或std::unordered_集进行基准标记。但是字符串不是alwa