当单个向量可以具有不同的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!
答案 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
视为不透明,并忽略任何分配
构建返回的向量所必需的。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
而没有进行复制。 tmp
有capacity == size
当且仅当make_vector
的返回值具有该属性时。
在#2,tmp
被复制到table
。 table
中新构建的副本有
根据需要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
被复制到table
。 table
中新构建的副本有
根据需要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。
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)
正如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);
}
输出:
+ 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
) - 有意创建的数据。复制或创建是指任何大数据以源大数据无法“丢弃”的方式复制的任何时间。移动指的是大数据移动的任何情况,源大数据可以被“丢弃”。
案例a
和b
,结果相同,可能是您想要的。请注意明确使用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_back
。 std::vector
调用tmp
的构造函数(不传递有关big_data_type
容量的信息),并隐含每个big_data_type
的副本。 (如果可以移动make_move_iterator(tmp.begin())
,我们可以使用N+1
等。)
请注意,无论我们如何对此进行编码,我们必须至少进行table
次分配(针对shrink_to_fit
及其每个元素)。这意味着capacity==size
的使用无济于事,因为它最好只进行一次分配和一次去分配(除非N+1
我们预计不会发生任何可能),相互抵消(因此,分配无法为所需的{{1}}总和做出贡献。这就是为什么其他一些答案是不可接受的原因。