如何最好地填充向量向量(避免浪费内存和不必要的分配和解除分配)?

时间:2013-08-12 14:19:00

标签: c++ c++11 vector allocation

当单个向量可以具有不同的size()时,我想填充向量的向量,例如

std::vector<std::vector<big_data_type> > table;
std::vector<big_data_type> tmp;
for(auto i=0; i!=4242; ++i) {
  tmp = make_vector(i);              // copy elison; calls new[] only for i=0
  table.push_back(tmp);              // copy         calls new[] each time
}

我的主要问题是避免在未使用的容量上浪费内存。所以我的第一个问题是:

Q1 副本(在push_back内制作)是capacity() == size()(我想要的),还是保留tmp是,或者这个实现依赖/未定义?

我正考虑将个人vector移到table

  table.push_back(std::move(tmp));   // move

但这肯定会保留capacity,从而浪费内存。此外,这不会避免分配每个单独的向量,它只会将其移动到另一个位置(在make_vector内而不是push_back)。

Q2 我想知道它省略变量tmp有什么不同,导致代码更优雅(2而不是5行):

for(auto i=0; i!=4242; ++i)
  table.push_back(make_vector(i));   // move!

我最初的想法是,这将在每次迭代时构造和销毁另一个临时值,从而生成对new[]delete[]的许多调用(这将基本上重用相同的内存)。但是,此外,这将调用push_back的移动版本,因此浪费内存(见上文)。正确的吗?

Q3 编译器是否有可能将我之前的代码“优化”为后一种形式,从而使用移动而不是复制(导致浪费内存)?

Q4 如果我是正确的,在我看来,这一切都意味着为临时对象自动移动数据是一种混合的祝福(因为它可以防止压缩)。是否有任何方法可以明确禁止移动最后一个被剪切的代码,例如

for(auto i=0; i!=4242; ++i)
  table.push_back(std::copy(make_vector(i)));   // don't move!

6 个答案:

答案 0 :(得分:3)

  

Q1复制(在push_back中制作)是否具有capacity()== size()(我想要的),或保留tmp所具有的,或者这个实现依赖/未定义?

标准从不设置容量的最大值,只有最小值。也就是说,大多数实现都会有capacity() == size()用于新的向量副本或容量略微向上舍入到分配器实现的块大小。

  

Q2我想知道省略变量tmp会有什么不同,从而产生更优雅的代码。

结果是进入table而不是复制。

  

Q3编译器是否有可能优化&#34;我之前的代码进入后一种形式,因此使用移动而不是复制(导致浪费内存)?

这是可能但非常不可能。编译器必须证明移动与复制没有明显的不同,这对我所知的当前编译器没有足够的挑战性。

  第四季如果我纠正了,在我看来,这一切意味着为临时物体自动移动数据是一种混合的祝福(因为它可以防止压缩)。

移动是速度优化,不一定是空间优化。复制可能会减少空间,但肯定增加处理时间。

如果您想优化空间,最好的办法是使用shrink_to_fit

std::vector<std::vector<big_data_type> > table;
for(auto i=0; i!=4242; ++i) {
  std::vector<big_data_type> tmp = make_vector(i); // copy elison
  tmp.shrink_to_fit();                             // shrink
  table.push_back(std::move(tmp));                 // move
}

编辑:深入分析。

假设:

  • table将提前保留其空间,因为它的大小已知,我们 因此,重点关注vector<big_data_type>的分配和解除分配 从make_vector返回的,暂时存储在tmp中, 最后在table
  • make_vector(i)的返回值可能有也可能没有capacity == size。 此分析将make_vector视为不透明,并忽略任何分配 构建返回的向量所必需的。
  • 默认构造的向量具有0容量。
  • 当且仅当reserve(n)时,
  • n才能将容量设置为n > capacity()
  • shrink_to_fit()设置capacity == size。它可能会也可能不会实施 要求复制整个矢量内容。
  • 矢量复制构造函数设置capacity == size
  • std::vector可能会也可能不会提供强大的例外保证 复制作业。

我将对两个正整数的分析进行参数化:N,数量为 在算法结束时将在table中的向量(OP中为4242), 和K:所有向量中包含的big_data_type个对象的总数 在算法过程中由make_vector生成。

你的技术

std::vector<std::vector<big_data_type> > table;
table.reserve(N);
std::vector<big_data_type> tmp;
for(auto i=0; i!=N; ++i) {
  tmp = make_vector(i); // #1
  table.push_back(tmp); // #2
}
// #3

对于C ++ 11

在#1处,由于tmp已经构建,因此无法进行RVO /复制省略。上 每次通过循环时,返回值都会分配给tmp。该 赋值是一个移动:tmp中的旧数据将被销毁(除了 tmp为空时的第一次迭代)和返回值的内容 make_vector移入tmp而没有进行复制。 tmpcapacity == size 当且仅当make_vector的返回值具有该属性时。

在#2,tmp被复制到tabletable中新构建的副本有 根据需要capacity == size。 #3 tmp可能会留下范围及其范围 存储被解除分配。

总分配/解除分配:N。所有分配在#2,N - 1解除分配在#1,一个在#3。

big_data_type个对象的总副本:K

对于Pre-C ++ 11

在#1处,由于tmp已经构建,因此无法进行RVO /复制省略。上 每次通过循环时,返回值都会分配给tmp。这个 如果(a),则赋值需要分配和释放 实施提供了有力的保证,或者(b)tmp太小了 包含返回向量中的所有元素。在任何情况下,元素必须 单独复制。在完整表达式结束时,临时对象 保存来自make_vector的返回值会被销毁,从而产生一个 解除分配。

在#2,tmp被复制到tabletable中新构建的副本有 根据需要capacity == size。 #3 tmp可能会留下范围及其范围 存储被解除分配。

总分配/解除分配:N + 1到2 * N。 1到N分配在#1,N分配在#2;                     N到2 * N - 1个解除分配在#1,1个在#3。

总份数:2 * K。 {1}位于#1,K位于#2。

我的技术(仅限C ++ 11)

K

#1 std::vector<std::vector<big_data_type> > table; table.reserve(N); for(auto i=0; i!=N; ++i) { auto tmp = make_vector(i); // #1 tmp.shrink_to_fit(); // #2 table.emplace_back(std::move(tmp)); // #3 } 是根据tmp的返回值新建的,所以 RVO /复制省略是可能的。即使执行make_vector 阻止RVO,make_vector将被移动构造,导致没有分配, 解除分配或复制。

在#2 tmp可能需要也可能不需要单个分配 deallocation,取决于shrink_to_fit的返回值是否已经 拥有make_vector属性。如果发生分配/解除分配,则 根据实施的质量,可能会也可能不会复制元素。

在#3处,capacity == size的内容被移动到一个新构造的向量中 tmp。不执行分配/解除分配/复制。

总分配/解除分配:0或table,当且仅当N未返回带make_vector的向量时,全部位于#2。 总份数:0或capacity == size,当且仅当K作为副本实施时,全部位于#2。

如果shrink_to_fit的实现者生成带有make_vector的向量 属性和标准库最佳地实现capacity == size 没有新闻/删除,也没有副本。

结论

My Technique的最坏情况表现与预期的案例表现相同 你的技术我的技术是条件优化的。

答案 1 :(得分:1)

除了Caseypost之外,我还有以下意见。

正如jrok在评论here中所述,shrink_to_fit无法保证做任何事情。但是,如果shrink_to_fit为exaclty size()个元素分配内存,复制/移动元素,并释放原始缓冲区,那么这正是OP的要求。

我对Q4的确切答案,即

  
    

是否有任何方法可以明确禁止移动最后一个代码剪切[...]?

  

是:是的,你可以做到

for(auto i=0; i!=4242; ++i)
  table.push_back(static_cast<const std::vector<big_data_type>&>(make_vector(i)));

OP建议的copy功能可以写成如下。

template <typename T>
const T& copy(const T& x) {
    return x;
}

,代码变为

for(auto i=0; i!=4242; ++i)
  table.push_back(copy(make_vector(i)));

但老实说,我认为这不是一件明智的事。

制作v的每个元素table的最佳位置,v.size() == v.capacity()位于make_vector()如果可能。 (作为Casey said,标准不会对容量设置任何上限。)然后将make_vector()的结果移动到table在两种意义上都是最佳的(内存和速度)。 OP的剪辑可能应该照顾table.size()

总之,该标准没有提供任何强制容量匹配大小的方法。 Jon Kalb有一个(明智的,恕我直言)suggestion使std::vector::shrink_to_fit至少与shrink_to_fit idiom一样有效(关于内存使用)(这也不保证任何事情) 。但是,委员会的一些成员并不热衷于此,并建议人们应该向他们的供应商抱怨或者实施他们自己的容器和分配功能。

答案 2 :(得分:1)

以下是一些运行时测试,其中包含一个辅助类型,用于计算创建,移动和复制:

#include <vector>
#include <iostream>

struct big_data_type {
  double state;
  big_data_type( double d ):state(d) { ++counter; ++create_counter; }
  big_data_type():state(0.) { ++counter; }
  big_data_type( big_data_type const& o ): state(o.state) { ++counter; }
  big_data_type( big_data_type && o ): state(o.state) { ++move_counter; }
  big_data_type& operator=( big_data_type const& o ) {
    state = o.state;
    ++counter;
    return *this;
  }
  big_data_type& operator=( big_data_type && o ) {
    state = o.state;
    ++move_counter;
    return *this;
  }
  static int counter;
  static int create_counter;
  static int move_counter;
};
int big_data_type::move_counter = 0;
int big_data_type::create_counter = 0;
int big_data_type::counter = 0;

std::vector<big_data_type>& make_vector( int i, std::vector<big_data_type>& tmp ) {
  tmp.resize(0);
  tmp.reserve(1000);
  for( int j = 0; j < 10+i/100; ++j ) {
    tmp.emplace_back( 100. - j/10. );
  }
  return tmp;
}
std::vector<big_data_type> make_vector2( int i ) {
  std::vector<big_data_type> tmp;
  tmp.resize(0);
  tmp.reserve(1000);
  for( int j = 0; j < 10+i/100; ++j ) {
    tmp.emplace_back( 100. - j/10. );
  }
  return tmp;
}
enum option { a, b, c, d, e };
void test(option op) {
  std::vector<std::vector<big_data_type> > table;
  std::vector<big_data_type> tmp;
  for(int i=0; i!=10; ++i) {
    switch(op) {
      case a:
        table.emplace_back(make_vector(i, tmp));
        break;
      case b:
        tmp = make_vector2(i);
        table.emplace_back(tmp);
        break;
      case c:
        tmp = make_vector2(i);
        table.emplace_back(std::move(tmp));
        break;
      case d:
        table.emplace_back(make_vector2(i));
        break;
      case e:
        std::vector<big_data_type> result;
        make_vector(i, tmp);
        result.reserve( tmp.size() );
        result.insert( result.end(), std::make_move_iterator( tmp.begin() ),std::make_move_iterator( tmp.end() ) );
        table.emplace_back(std::move(result));
        break;
    }
  }
  std::cout << "Big data copied or created:" << big_data_type::counter << "\n";
  big_data_type::counter = 0;
  std::cout << "Big data created:" << big_data_type::create_counter << "\n";
  big_data_type::create_counter = 0;
  std::cout << "Big data moved:" << big_data_type::move_counter << "\n";
  big_data_type::move_counter = 0;
  std::size_t cap = 0;
  for (auto&& v:table)
    cap += v.capacity();
  std::cout << "Total capacity at end:" << cap << "\n";
}

int main() {
  std::cout << "A\n";
  test(a);
  std::cout << "B\n";
  test(b);
  std::cout << "C\n";
  test(c);
  std::cout << "D\n";
  test(d);
  std::cout << "E\n";
  test(e);
}

Live example

输出:

+ g++ -O4 -Wall -pedantic -pthread -std=c++11 main.cpp
+ ./a.out
A
Big data copied or created:200
Big data created:100
Big data moved:0
Total capacity at end:100
B
Big data copied or created:200
Big data created:100
Big data moved:0
Total capacity at end:100
C
Big data copied or created:100
Big data created:100
Big data moved:0
Total capacity at end:10000
D
Big data copied or created:100
Big data created:100
Big data moved:0
Total capacity at end:10000
E
Big data copied or created:100
Big data created:100
Big data moved:100
Total capacity at end:100

E是一个可以移动大数据的示例,通常不起作用。

created仅指明确创建的数据(即来自double) - 有意创建的数据。复制或创建是指任何大数据以源大数据无法“丢弃”的方式复制的任何时间。移动指的是大数据移动的任何情况,源大数据可以被“丢弃”。

案例ab,结果相同,可能是您想要的。请注意明确使用tmp vector作为make_vector的参数:elision不会让您重用缓冲区,您必须明确它。

答案 3 :(得分:1)

向量构造向量在顶层向量的末尾只添加数据的情况下会带来许多不必要的开销(这里似乎就是这种情况)。

主要问题是顶层向量中每个条目的单独缓冲区分配和管理。

如果可能的话,最好将所有子条目连接到一个连续的缓冲区中,并使用单独的缓冲区为每个顶级条目索引。

请参阅this article(在我的博客上),有关此问题的更多讨论,以及“折叠向量向量”类的示例实现,以将此类索引缓冲区设置包装在通用容器对象中。 / p>

正如我之前所说的,这仅适用于数据仅在数据结构的末尾添加的情况,即您之后不再返回并将条目推送到任意顶级子向量,但是在应用这种技术的情况下,这可能是一个非常重要的优化。

答案 4 :(得分:0)

一般情况下,如果你想要容量大小相同,你可以使用vector :: shrink_to_fit() http://www.cplusplus.com/reference/vector/vector/shrink_to_fit/

答案 5 :(得分:0)

好吧,我想我学到了一点,但真的找不到完整的答案。因此,让我们首先澄清任务

我们有一个填充向量的函数。为了避免关于复制省略是否可能的争论,让我们假设它的定义是

void fill_vector(std::vector<big_data_type>& v, int i)
{
  v.clear();
  v.reserve(large_number);       // allocates unless v.capacity() >= large_number
  for(int done=0,k=0; k<large_number && !done; ++k)
    v.push_back(get_more_big_data(i,done));
  // v.capacity() == v.size() is highly unlikely at this point.
}

此外,我们想填写表格

std::vector<std::vector<big_data_type>> table;
带有N条目的

,每个条目由fill_vector()生成,以便(1)最小化表中的内存使用,但(2)避免不必要的分配/解除分配。在简单的C代码中,N+2分配和1取消分配,K实际提供的big_data_type fill_vector()总数仅为table.reserve(N); // allocates enough space for N vectors size_t K=0; // count big_data_types in table std::vector<big_data_type> tmp; for(int n=0; n!=N; ++n) { fill_vector(tmp,i); // allocates at first iteration only K += tmp.size(); table.push_back(tmp.begin(),tmp.end()); // allocates tmp.size() big_data_type } // de-allocates tmp 分配。使用C ++我们不需要更多。这是一个可能的C ++ 答案

N+2

因此,我们根据需要进行了1次分配和K解除分配,并且没有浪费内存(不超过big_data_type中分配的table push_backstd::vector调用tmp的构造函数(不传递有关big_data_type容量的信息),并隐含每个big_data_type的副本。 (如果可以移动make_move_iterator(tmp.begin()),我们可以使用N+1等。)

请注意,无论我们如何对此进行编码,我们必须至少进行table次分配(针对shrink_to_fit及其每个元素)。这意味着capacity==size的使用无济于事,因为它最好只进行一次分配和一次去分配(除非N+1我们预计不会发生任何可能),相互抵消(因此,分配无法为所需的{{1}}总和做出贡献。这就是为什么其他一些答案是不可接受的原因。