C++ &引用;“扁平化”;标准:设置<;标准::字符串>;用于存储和比较?

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::set已经有了非常好的比较运算符,但我认为我可能会针对我的特定用例进行优化,并希望确保我不会以某种方式伤害自己

本质上,我有一个代价高昂的操作,它将std::set&作为输入。我正在缓存操作的结果,这样如果已经传入了相同的输入,我就可以返回结果。这确实需要存储集合的副本(我在

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 缓存,与比较。

< P>我会考虑在SET上写一个小包裹,以跟踪它的地址和版本号。它包括O的重载。修改集合的操作(插入、擦除等),当插入/擦除发生时,会增加版本号

然后,要确定相等性,您只需看两件事:集合的地址和版本号。如果修改非常少,并且相等性测试非常常见,那么比较所节省的时间可能比跟踪更改所花费的时间要多得多——瞧,您会获得一个快速的胜利

如果您必须编写一个完整的包装器(一个公开所有
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