“展平”std :: set <std :: string>进行存储和比较?

时间:2017-09-12 12:25:06

标签: c++ string performance stl set

这可能是一个愚蠢的问题,基于std :: set&lt;&gt;已经有了非常好的比较运算符,但我想我可能会对我的特定用例进行优化,并希望确保我不会以某种方式伤害自己。

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

中进行)
std::map<std::set<std::string>, Result*>

,然后每次调用操作时都进行搜索。由于很可能连续几次调用相同的操作,我会说高速缓存的std :: set被发现> 99%的时间。我最近尝试了我认为可能是一个小改进,基于传入字符串中某些字符无效的事实:我将std :: set压平为单个字符串,组件字符串用'分隔: '性格。我的std :: map然后变成

std::map<std::string, Result*> 

并且每次调用操作时,该集合都被展平并且在缓存中搜索单个字符串。

我实际上对性能提升感到惊讶。我的测试运行使用了包含5个字符串的std :: sets,每个字符串长30个字符,以及10,000,000次搜索。在我的工作站上,每次运行的时间是

 std::map<std::set<std::string>, Result*> : 138.8 seconds
 std::map<std::string, Result>            : 89.2  seconds

看起来,即使每次调用都会使套装变平,第二种方法也是一个巨大的改进。我想我的问题是:为什么?我在这里做了一些可能很糟糕的事情,std :: set的实现者有目的地避免了(即可能导致使用更大的字符串的坏堆碎片?)这只是因为集合中的各个字符串位于不同的位置并且必须单独进行比较?我在脚下射击自己了吗?在这种特定情况下,似乎显而易见的是改进了这种性能提升。

2 个答案:

答案 0 :(得分:4)

  

为什么?

数据位置。

std::set通常实现为二进制搜索树。与std::string相比,使用std::set在您的计算机上进行缓存可能会使搜索操作更快。

答案 1 :(得分:0)

我会考虑在set周围编写一个小包装器来跟踪其地址和版本号。它将包括修改集合的操作的重载(插入,擦除等),并且当插入/擦除发生时,它会增加版本号。

然后,为了确定平等,你只看两件事:集合的地址和版本号。如果修改是相当罕见的并且相等的测试相当普遍,那么比较时节省的时间可能远远大于跟踪更改所花费的时间 - IOW,你获得了很大的速度胜利。

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