【问题标题】:"Flattening" std::set<std::string> for storage and comparison?“扁平化” std::set<std::string> 用于存储和比较?
【发布时间】:2017-09-12 12:25:06
【问题描述】:

这可能是一个愚蠢的问题,因为 std::set 已经有非常好的比较运算符,但我认为我可能会对我的特定用例进行优化,并希望确保我没有受到伤害我自己。

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

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 的实现者故意避免了(即可能导致较大的字符串出现坏的堆碎片?)仅仅是因为集合中的各个字符串位于不同的位置并且必须单独比较?我是在踢自己的脚吗?在这种特定情况下,这种性能提升似乎太明显了。

【问题讨论】:

  • 如果您在 99% 的情况下使用相同的参数调用函数,那么我会说调用者有问题,而不是函数本身有问题。无论如何,您不能在您的集合中添加某种 id,以便该方法只需要比较 id 而不是整个 set 吗?听起来你正在传递的集合并不经常改变。
  • 我做了一点简化,函数的输入是 std::set 和要比较的 2 个单独的消息。该集合描述了在比较之前应用于消息的转换,并且它正在构建这个转换是昂贵的部分(应用它是微不足道的)。集合几乎总是不变,但消息几乎总是不同的。理想情况下,我会让调用者以某种方式获取转换的句柄,然后在调用比较时使用句柄而不是集合 - 不幸的是,这需要替换现有代码。
  • 只要确保你的分隔符不能是实际字符串的一部分,你应该没问题。此外,无论何时,不要忘记使用 std::unordered_map 或 std::unordered_set 进行性能测试。然而,字符串并不总是存储在其中的最佳类型,因为您必须读取整个字符串才能生成哈希,而操作符
  • 考虑到它是一个查找,unordered_map 可能更有效。此外,当使用字符串作为键时,不需要按字母顺序,首先比较字符串长度会更有效。 IE。将“z”排在“aa”之前。

标签: c++ string performance stl set


【解决方案1】:

为什么?

数据局部性。

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

【讨论】:

  • 基本上一个字符串可以保留在CPU缓存中,因此搜索它可以更快,而一个集合不能(它在内存中是稀疏的)。有关“数据局部性”的更多信息:gameprogrammingpatterns.com/data-locality.html
  • @roalz 嗬,我明白了。谢谢。
【解决方案2】:

我会考虑为 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";
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-28
    • 1970-01-01
    • 1970-01-01
    • 2010-11-06
    • 1970-01-01
    • 1970-01-01
    • 2022-08-18
    • 2013-09-12
    相关资源
    最近更新 更多