【问题标题】:STL containers and large amounts of dataSTL 容器和大量数据
【发布时间】:2014-09-19 20:56:15
【问题描述】:

我有大量数据被读入内存 - 暂时,但对系统来说是必需的。

我一直在检查std::vector 和std::unordered_map 的性能。
对于std::vector,我使用了struct 类型:

struct information{
    std::string name;
    unsigned int offset;
}

对于std::unordered_map,我使用std::string 作为键,使用unsigned int offset 作为值。

假设,其中 2 000 000 个已加载到内存中,我尝试了以下操作并得到了以下结果:

std::vector:

在随机字符串上,如果在向量上调用了保留,则永远不会超过 32 个字符。

std::vector<information> vec;
vec.reserve(2500000);

插入

vec.push_back({dataName, offset});

相当快。但是,尝试查找数据非常缓慢。发现是这样实现的:

auto it = std::find_if(vec.begin(), vec.end(), [&name](information &info) -> bool {return info.name == name); });

看到它是一个大向量并且在名称比较中找到正确的struct,这是有道理的。但这是极其糟糕的表现。使用的内存很好 - 我认为内存增长的一部分是由于 std::string 大小调整造成的。

我对向量实现的问题是:有没有办法增加查找时间?我知道可以对向量进行排序以增加您的查找时间,但是您会浪费时间对向量进行排序。尤其是在这种大小的向量上。

std::unordered_map:

插入

std::unordered_map<std::string, unsigned int> unordMap;
unordMap.reserve(2500000);
unordMap.emplace(name, offset);

需要很长时间。当预先保留空间以尝试缩短插入时间时,会发生以下情况:

不调用reserve的时候插入结束的内存要多很多,不调用reserve的内存还是比vector的实现多很多。保留并没有真正改善插入时间。

当然,查找速度非常快。我对std::unordered_map 的问题是可以提高插入时间和内存使用率吗?

如果这些都做不到,那么我的下一个问题可能会很明显。有没有办法在这两个数据结构之间得到结果?什么最适合大量数据?

【问题讨论】:

  • 您失去对向量进行排序的时间只有一次,如果您进行大量查找,则排序将不会明显。您可以稍后使用 二分搜索 进行查找。
  • 我相信你打错了push_back()那里
  • 另外,虽然你选择unordered_map这一事实可能暗示了这一点,但我还是会问:你能有重复的字符串吗?
  • “有没有办法增加查找时间”...你的意思是查找速度?
  • 你在使用 Visual Studio 吗?

标签: c++ c++11 vector stl unordered-map


【解决方案1】:

好吧,这里的最佳解决方案是创建 std::map,它在插入和查找中的复杂性都是对数的。虽然,我看不出你为什么不使用 std::vector 的任何理由。使用快速排序甚至对 2M 元素进行排序时,速度非常快,尤其是如果您这样做一次。 std::binary_search 然后真的很快。如果您需要在插入之间进行大量查找,请考虑一下。

【讨论】:

  • 在上述情况下,std::map 会比std::unordered_map 更快吗?
  • 但是地图有很多内存开销。不仅仅是一个向量。我将研究对向量进行排序。
【解决方案2】:

大向量的问题是当你不知道你想要的对象的索引时的查找时间。正如您所说,改进它的一种方法是保留有序向量并对其进行二进制搜索。这样一来,查找时间将不是线性复杂度,而是对数复杂度,这在非常大的容器中节省了大量时间。这是std::map 中使用的查找(已订购)。您可以在您的std::vector 上使用std::lower_bound 或std::equal_range 进行类似的二分搜索。

大型无序映射的问题完全不同:这种容器使用哈希函数和模数计算,以便根据元素的键将元素放置在标准数组中。因此,当您在 std::unordered_map 中有 n 个元素时,您不太可能只需要一个 n-elements-long 数组,因为某些索引不会被填充。您将至少使用哈希和模产生的最大索引。提高内存使用率和插入时间的一种方法是编写自己的哈希函数。但这可能很难,具体取决于您使用的字符串类型。

【讨论】:

  • std::hash 在std::size_t 的整个范围内生成值,而std::unordered_map 在其存储桶数组中没有那么多元素...
  • @Yakk:谢谢,已修复。但是,我看不出您的解决方案如何比直接使用 lower_bound 或 equal_range 更快地进行查找。您能解释一下吗(在此处、在您的回答中或通过消息传递)?
  • 我为什么要散列?好吧,比较两个std::strings 很慢(您必须访问不在附近的内存(命中),然后您必须遍历它直到它们不同意和/或到达终点(不可预测的分支))。比较数组中的两个std::size_ts 很快(缓存一致性!)。你仍然会得到分支预测失败(假设数据在容器中的随机点,这很可能),但它们的数量是有限的。或者你说的是information_searcher?这样我们就不必将std::string 复制到information 中。
  • @Yakk :哦,我知道它现在是如何工作的了。比较size_t 确实比string 快。这对于这种特殊用途来说非常专业,但我仍然学到了一些东西。关于您的information_searcher 的最后一件事:不能像这样调用tie 来代替它吗? std::tie(std::hash(name), name)
  • 试一试 -- 需要强制转换以获取信息或在比较器上进行 4 次重载。
【解决方案3】:

vector 通常被实现为“动态数组”并且应该是最节省内存的。 通过良好的预订策略,它可以插入 O(1) = 快速。搜索是 O(n) = 非常糟糕。

您可以通过对其进行排序来帮助矢量(如果您首先加载然后搜索,那么我认为它会是最好的 - std::sort + std::binary_search)。
您也可以使用 std::lower_bound 实现类似插入排序的功能。插入 = O(log n) = 好,搜索 = O(log n) = 好

map(有序)实际上可以做同样的工作,但也可以使用树来实现 = 内存效率较低,访问与排序向量一样好(但可能更少重新分配,但在你的情况下,排序向量仍然是最好的)

unordered_map 通常使用哈希表实现 = 一些内存开销但操作速度很快(插入不能像未排序的向量那样快,但应该仍然非常快)。散列的问题在于它可以很快,甚至最快,但也可能是最差的解决方案(在极端条件下)。上述结构(有序向量和地图/树是稳定的,总是表现相同 - 对数复杂度)。

【讨论】:

    【解决方案4】:
    struct information{
      std::string name;
      unsigned int offset;
      information(information const&)=default;
      information(information&&)=default;
      information(std::string n, unsigned o):name(std::move(n)),offset(o),hash(std::hash<std::string>()(name)) {};
      information():information("",0) {};
      bool operator<( information const& o ) const {
        return tie() < o.tie();
      }
      std::tuple<std::size_t, std::string const&> tie() const { return std::tie(hash, name); }
    private:
      std::size_t hash;
    };
    

    将上述结构用于您的std::vector。

    添加所有数据后,std::sort它。

    要查找与name 匹配的内容,请执行以下操作:

    struct information_searcher {
      struct helper {
        std::tuple<std::size_t, std::string const&> data;
        helper( std::string const& o ):data(std::hash<std::string>()(o), o) {};
        helper( helper const& o ) = default;
        helper( information const& o ):data(o.tie()) {}
        bool operator<( helper const& o ) const { return data < o.data; }
      };
      bool operator()( helper lhs, helper rhs ) const { return lhs < rhs; }
    };
    
    information* get_info_by_name( std::string const& name ) {
      auto range = std::equal_range( vec.begin(), vec.end(), information_searcher::helper(name), information_searcher{} );
      if (range.first == range.second) {
        return nullptr;
      } else {
        return &*range.first;
      }
    }
    

    这几乎是零开销的查找。

    我们在这里做的是对字符串进行哈希处理(用于快速比较),如果发生冲突,则使用std::string 比较。

    information_searcher 是一个让我们无需创建information(这将需要浪费分配)来搜索数据的类。

    get_info_by_name 返回一个指针 -- 如果没有找到 nullptr,否则返回一个指向具有该名称的第一个元素的指针。

    更改information.name 是不礼貌的,并且会使hash 字段不正确。

    这可能比简单的std::vector 版本使用更多的内存。

    一般来说,如果您的工作包括“将一堆东西添加到表中”然后“进行一堆查找”,那么最好的办法是构建一个 std::vector,以快速的方式对其进行排序,然后使用equal_range 进行查找。 map 和 unordered_map 针对大量混合插入/删除/等进行了优化。

    【讨论】:

    猜你喜欢
    • 2014-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-13
    • 1970-01-01
    • 2013-04-16
    • 1970-01-01
    • 2011-03-22
    相关资源
    最近更新 更多