优雅的方式,在两个向量中空间高效地解包对矢量

时间:2016-09-21 17:20:21

标签: c++ performance c++11 memory vector

我有一个std::pair<std::string,size_t>元素的大向量,我想用两个向量解压缩它使用额外的内存开销(我不希望内存空间占用加倍,即在解包后擦除对的向量并且可能尽可能快。以下解决方案速度慢得令人无法接受:

std::vector<std::pair<std::string, size_t>> string_weight;
get_from_file("mybigfile.txt", string_weight); //it just fills the string_weight vector
//... do stuff...
std::vector<std::string> strings;
std::vector<size_t> weights;
for (auto it = string_weight.begin(); it != string_weight.end() ; it = string_weight.erase(it)) {
     strings.push_back(std::move(it->first));
     weights.push_back(std::move(it->second));
}

因此我尝试修改以前的解决方案,只需按以下方式更改for循环:

for (auto it = string_weight.begin(), it2 = it; it != string_weight.end() ; it = string_weight.erase(it, it2)) {
        size_t delta = 100000;
        for ( it2 = it ; it2 != string_weight.end() && it2 != it+delta; it2++ ) {
            strings.push_back(std::move(it2->first));
            weights.push_back(std::move(it2->second));
        }
    }

这个更快,但完成时间与我为delta选择的值成正比,我不喜欢它。你能帮我解决一下或指出一些有用的技巧吗?

提前谢谢。

7 个答案:

答案 0 :(得分:6)

试试这个:

std::vector<std::string> strings;
std::vector<std::size_t> weights;

strings.reserve(string_weight.size());
weights.reserve(string_weight.size());

for (auto & p : string_weights)
{
     strings.push_back(std::move(p.first));
     weights.push_back(p.second);
}

一些变化:

  • 预构建权重向量:

    std::vector<std::size_t> weights(string_weight.size());
    
    // ...
    
    weights[i] = string_weights[i].second;
    

    这可能会更好,因为它可以避免重复的尺寸检查,但会使您最初的清零成本。 (使用原始动态数组或非构造分配器可以避免这种情况。)

  • 预构建字符串向量:

    std::vector<std::string> strings(string_weight.size());
    
    // ...
    
    strings[i] = std::move(string_weights[i].first);
    // or
    strings[i].swap(string_weights[i].first);
    

    同样,这可以避免重复的范围检查。

答案 1 :(得分:2)

logback.xml开头的删除元素是昂贵的操作。您可以使用3种可能性来加快速度:

  1. 使用std::vector代替std::deque代表对,它有O(1)用于删除前面的元素
  2. 清除循环后的对矢量

  3. 提前调整目标矢量大小并向后复制元素

  4. 示例:

    std::vector

答案 2 :(得分:2)

std::vector中删除元素不会释放任何内存(由容器本身直接拥有)。即使它确实释放了内存(可以通过在erase()之后调用shrink_to_fit()来实现)仍然需要临时(大约)加倍内存使用量,因为重新分配需要发生类似于如何std::vector在成长时调整大小 - 需要分配一个新的(稍微更小的)内存块,将元素复制到该新区域,然后才会释放旧的分配。

因此(除非您可以使用std::vector替换源std::deque),否则您应该忘记在此转换期间降低高水位内存使用量。

答案 3 :(得分:1)

首先应该尝试移动内容。通常std::string使用的大部分空间都不在字符串本身内。

所以你只是:

template<template<class...>class Tuple, class...Ts>
using vec_of_tup = std::vector<Tuple<Ts...>>;
template<template<class...>class Tuple, class...Ts>
using tup_of_vec = Tuple< std::vector<Ts>... >;

vec_of_tup<std::pair, std::string, std::size_t> in;
tup_of_vec<std::pair, std::string, std::size_t> out;

out.first.reserve(in.size());
out.second.reserve(in.size());

for(auto&& e:in) {
  out.first.push_back( std::move(e.first) );
  out.second.push_back( std::move(e.second) );
}
decltype(in){}.swap(in); // forced clear

这确实使用了更多的峰值记忆,因为两个载体同时存在。但是,当我们将它从一个容器移动到另一个容器时,用于字符串的数据(超过一定的短尺寸)不会被双重分配。

只有字符串的“簿记”数据保持两次左右。

避免这种情况几乎是不可能的。缩小源向量使用的内存需要重新分配新大小的缓冲区。如果在将K个元素复制到目标向量后执行此操作,则直接在向量中使用的内存为N + K之前。您必须创建一个大小为(N-K)的新缓冲区来将元素复制到。所以你使用N + K + N-K = 2N内存。

如果您使用2N内存,您可以使用上述解决方案并避免不必要的副本。

您的代码似乎在字符串的“簿记”部分使用大约2.8N内存,而使用N ^ 2 / K元素副本。这太糟糕了。

可能你的问题是你正在使用std::vector来获得可笑的大N.当你使用的矢量大小接近系统上可用的内存时,std::vector的好处开始消失。

一种方法可能类似于实现具有受控块大小的双端队列,而不是默认的小型。每个块说10页内存。

现在从前面/后面擦除是有效的,并且你的内存与每一千个元素读取的一个页面错误相当连续,而不是每个元素读取的页面错误的基于纯节点的容器。您可以移动部分容器而不会丢失内存,就像移动块时它被释放一样。

您的优化路线很充实,找到另一条路线。

这是一个初始峰值。这不是例外安全:

template<class T, std::size_t block_size_guess = 10*4096>
struct block_vector {
  template<class...Args>
  void emplace_back( Args&&...args ) {
    if (!last_block_used) {
      blocks.emplace_back();
    }
    new( (void*)get_ptr( size() ) ) T(std::forward<Args>(args)...);
    last_block_used = (last_block_used+1)%block_size;
  }
  template<class...Args>
  void emplace_front( Args&&...args ) {
    if (!first_block_unused) {
      blocks.emplace_front();
      first_block_unused = block_size;
    }
    --first_block_unused;
    new( (void*)get_ptr( 0 ) ) T(std::forward<Args>(args)...);
  }

  std::size_t size() const {
    if (last_block_used) // if zero, it means the last block is full
      return blocks.size() * block_size - first_block_unused + last_block_used - block_size;
    else
      return blocks.size() * block_size - first_block_unused;
  }
  T& operator[]( std::size_t i ) { return *get_ptr(i); }
  T const& operator[]( std::size_t i ) const { return *get_ptr(i); }
  // todo: iterators, front(), back(), erase( it, it ), erase( it ), etc.
private:
  enum {
    block_calc = block_size_guess/sizeof(T),
    block_size = block_calc?block_calc:1,
  };
  using raw_block = std::array< std::array<unsigned char, sizeof(T)>, block_size >;
  std::deque<raw_block> blocks;
  std::size_t first_block_unused = 0;
  std::size_t last_block_used = 0;

  using block = std::array< T, block_size >;

  block& get_block( std::size_t b ) {
    return reinterpret_cast<block&>(blocks[b]);
  }
  block const& get_block( std::size_t b ) const {
    return reinterpret_cast<block const&>(blocks[b]);
  }
  static std::size_t outer( std::size_t i ) { return (i+first_block_unused)/block_size; }
  static std::size_t inner( std::size_t i ) { return (i+first_block_unused)%block-size; }
  T* get_ptr( std::size_t i ) {
    return std::addressof( get_block( outer(i) )[ inner(i) ] );
  }
  T const* get_ptr( std::size_t i ) const {
    return std::addressof( get_block( outer(i) )[ inner(i) ] );
  }
};

答案 4 :(得分:0)

或者,要创建新矢量,您可以创建视图。所以使用range-v3,您可以这样做:

const auto strings = string_weights | ranges::view::keys;
const auto weights = string_weights | ranges::view::values;

Demo

答案 5 :(得分:0)

很抱歉在这里成为厄运的预兆,但是,

如果您受内存限制,则需要:

  1. 批处理文件,或
  2. 如果可能,请将该文件视为随机访问数据存储。
  3. 除非您在目标向量中有reserve个内存,否则它们可能会在填充时重新调整大小几次。这不仅会过度分配内存,还会鼓励碎片 - 连续内存分配的敌人(std::vector依赖于此标准所规定的)。

    此外,一旦填充了初始向量(即使您通过保留适当数量的空间有效地完成了它),它也不会在不调用shrink_to_fit的情况下收缩。即便如此,也无法保证它会缩小。更糟糕的是,如果这样做,那么在操作过程中会消耗更多的内存。

答案 6 :(得分:0)

如果不是C ++中的所有抽象级别,都可以就地完成

对于C ++中std::vector拥有的内存,这当然是不可能的。即使在汇编中,您也需要一种方法来告诉您的内存分配器,您的一个大分配现在是两个较小的分配。如果您希望 在C ++中能够做到这一点,则可以使用数组或其他东西的组合,或者使用新的放置方式来重新分配从分配器获得的原始内存。

我发布此内容只是为了说明使有效代码无法实现的抽象级别是多么糟糕。 (例如,C ++ new / delete甚至没有try_realloc可以让vector attatt 进行就地增长或缩小;所有主流C ++实现都有一个std :: vector只是分配和复制,甚至在增长时都不会尝试查看是否有与现有分配相邻的空闲地址空间。)IDK为什么ISO C ++仍未添加任何可以进行有效内存管理的内容(除了std::malloc / {{ 1}}与new / delete不兼容。

如果您想在不编写新元素的情况下增加std :: vector(因为您想将std::realloc放入该内存或其他内容),则需要使用自定义分配器的vector来构造元素。 / p>


read()可以被std::string编辑,这归结为复制对象表示形式。 std::move可以复制。

给出对象对的大小比,计算第一个输出数组末尾的字节偏移量。例如假设一个32字节的size_t(3个指针加上行内小字符串的额外空间,例如x86-64上的libstdc ++使用)和一个8字节的std::string,比率为4:1。假设对齐不需要每对中的额外填充,这意味着源向量具有40个字节的元素。

因此,对于10M元素源数组(400M字节),字符串数组将为10M元素,即320M字节,而size_t数组将为80M字节。

完成后,字符串数组将成为对数组中的前320MB,而size_t数组将成为下一个80MB。我们可以提前计算指向该目的地的指针。该位置也是也是在覆盖内存之前需要读取的一对的开始

我将首先说明一对对象中1:1的比例。

  • 加载2对,从整体的开始,以及从第二个数组开始的偏移量开始。 (即size_t
  • 将每个tmp对的第一个成员存储到第一对的位置。
  • 将每个tmp对的第二个成员存储到第二个tmp对的空间中。

重复直到完成,增加位置。为此分配或接触任何新内存将浪费CPU周期,并会导致不必要的内存压力。在具有SIMD的CPU上,可以很好地矢量化这些负载和混洗,并希望它们以接近记忆的速度运行,并且可以充分受益于缓存中的目标位置而不是复制到新的冷存储器中。

对于大小不一的对,您需要加载例如从中间开始每对1对,开始4对。 4x 40 = 5x 32,因此我们再次可以放回新数据,而不会覆盖尚未读取的任何内容。较高的部分是1x 40B = 5x 8B。

在C ++中,您可以使用本地tmp数组,而编译器仍然可以合理地将其优化为小数组大小的寄存器。


或者,首先不要创建巨大的成对向量。以中等大小的块读取文件(例如8kiB是一个合适的大小,可以轻松地适合L1d缓存),并将行解析为2个向量。