考虑这两个不同的函数实现,从前面删除x
个元素:
template <typename T>
std::vector<T> drop(int size, const std::vector<T>& coll){
if (size<0) return std::vector<T>();
auto sized = size > coll.size() ? coll.size() : size;
typename std::vector<T>::const_iterator first = coll.begin()+sized;
typename std::vector<T>::const_iterator last = coll.end();
return std::vector<T>(first,last);
}
template <typename T>
std::vector<T> drop2(int size, std::vector<T> coll){
if (size<0) return std::vector<T>();
auto sized = size > coll.size() ? coll.size() : size;
coll.erase(coll.begin(),coll.begin()+sized);
return coll;
}
在两个版本中,分配了一个新的std::vector
(在第二个版本中,它被复制为参数,而不是参考)。在一个中,结果由erase()
创建,而在另一个中,结果是使用原始向量的迭代器创建的。
有没有理由相信其中一个在性能方面会有显着差异?
另外,RVO是其中一种还是两种的保证吗?
编辑:
这是我做的一个测试,它显示第一个测试比第二个慢
template<typename F>
void dropExample(F f){
std::cout<<"drop example"<<std::endl;
auto t1 = Clock::now();
for (auto x: range(100000)){
f(2, range(100));
}
auto t2 = Clock::now();
std::cout << "Delta t2-t1: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
<< " ms" << std::endl;
}
输出:
dropExample(drop<int>);
dropExample(drop2<int>);
drop example
Delta t2-t1: 625 ms
drop example
Delta t2-t1: 346 ms
无论我在for
循环中添加了多少次迭代,数字大致都是这样,即使是数十秒的操作也是如此。
编辑2:
我已经使用左值增加了测试,如评论中所示:
template<typename F, typename T>
void dropExample2(F f, T vec){
std::cout<<"drop example 2"<<std::endl;
auto t1 = Clock::now();
for (auto x: range(1000)){
f(2, vec);
}
auto t2 = Clock::now();
std::cout << "Delta t2-t1: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
<< " ms" << std::endl;
}
然后在主要:
int main(int argc, const char * argv[]) {
auto testrange=range(100000);
dropExample(drop<int>);
dropExample(drop2<int>);
dropExample2(drop<int>,testrange);
dropExample2(drop2<int>,testrange);
return 0;
}
输出仍然表明第二个更快:
drop example
Delta t2-t1: 564 ms
drop example
Delta t2-t1: 375 ms
drop example 2
Delta t2-t1: 2318 ms
drop example 2
Delta t2-t1: 698 ms
以下是示例中使用的补充函数:
std::vector<int> range(int start, int end, int step);
std::vector<int> range(int start, int end){
if (end<start){
return range(start,end,-1);
}else if (start == end){
return std::vector<int> {start};
}else{
std::vector<int> nums(end-start);
std::iota(nums.begin(),nums.end(),start);
return nums;}
}
std::vector<int> range(int end){
return range(0,end);
}
std::vector<int> range(int start, int end, int step){
std::vector<int> nums{start};
auto next=start+step;
while ((next<end&&start<=end&&step>0)||
(next>end&&start>end&&step<0))
{
nums.push_back(next);
next+=step;
}
return nums;
}
答案 0 :(得分:3)
第一个几乎肯定更快,除非你喂drop
一个左值,在这种情况下你必须测量。
假设您有N个要素开始,要删除M个元素:
答案 1 :(得分:2)
你的第二个例子将创建一大堆对象(在复制输入参数时),以便稍后(在调用erase时)去掉它们。因此而产生的性能差异将取决于T
是什么,但我怀疑第一个会慢一些
此外,在第二个版本中使用的内存量会更大,因为擦除不会重新分配内存。
修改强>
您当前的测试存在缺陷,因为您将向量作为临时子集传递,允许编译器移动构造drop2
中的输入参数,从而完全删除副本。简单地改变:
for (auto x: range(100000))
f(200, range(10000));
到
auto v = range(10000);
for (auto x: range(100000))
f(200, v);
非常显着地改变了结果。然而,第二种方法对我来说仍然更快,直到矢量更大。同样值得注意的是,因为您使用的是int
,所以可以针对memcpy
以及几个指针操作优化不同的方法。
drop
可能只是(coll.size() - size) * sizeof(int)
个字节的memcpy,而drop2
可能会成为coll.size() * sizeof(int)
个字节的memcpy。这是因为int
的析构函数是无操作,因此对erase的调用可能会简单地从向量的size
指针中减去__last
。
如果您感兴趣的是这样的原始类型,那么没关系,但是如果您还希望有一个最佳实现,比如std::string
那么它的析构函数和复制构造函数就变得非常重要因素。我尝试使用std::vector<int>
作为向量中的类型,虽然总体上较慢,但对于较小的大小,似乎drop2
仍然更快。但是,drop
在较低阈值时变得更有效率。我非常怀疑这就是我们在这里看到的,所以我们最终运行的代码处于某种中间状态,只是memcpy
并且是我们逐字写的。< / p>
我想最终我们正在测试编译器优化不同函数的能力(std::uninitialized_copy
,std::move
(基于迭代器的函数),在一个函数中调用get_allocator().destroy(p)
循环于平凡和非平凡的类型等...)。在这一点上我只能说,结果在优化的方面和代码中看似微小的变化有多大差异。
然而,我仍然感到惊讶drop2
的运行速度超过drop
,即使只是针对特定尺寸的范围。
答案 2 :(得分:0)
真的,答案在于两个版本的广泛基准。
但是,进行评估后,我倾向于认为第一个版本会更快,因为在一般情况下你必须将较少的元素从初始向量复制到输出向量,而不是第二个你复制它们的地方。