迭代器范围矢量构造或矢量擦除速度更快吗?

时间:2015-12-11 20:06:25

标签: c++

考虑这两个不同的函数实现,从前面删除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;
}

3 个答案:

答案 0 :(得分:3)

第一个几乎肯定更快,除非你喂drop一个左值,在这种情况下你必须测量。

假设您有N个要素开始,要删除M个元素:

  • 第一个复制N-M个元素;第二个复制N个元素。
  • 第一个不执行任务;第二个执行N-M移动分配,可能会退化以复制分配。
  • 第一个可能,实际上也是RVO。第二个需要一个不能被忽略的举动。然而,移动矢量足够便宜,额外成本可能很小。

答案 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_copystd::move(基于迭代器的函数),在一个函数中调用get_allocator().destroy(p)循环于平凡和非平凡的类型等...)。在这一点上我只能说,结果在优化的方面和代码中看似微小的变化有多大差异。

然而,我仍然感到惊讶drop2的运行速度超过drop,即使只是针对特定尺寸的范围。

答案 2 :(得分:0)

真的,答案在于两个版本的广泛基准。

但是,进行评估后,我倾向于认为第一个版本会更快,因为在一般情况下你必须将较少的元素从初始向量复制到输出向量,而不是第二个你复制它们的地方。