在调用C ++ / STL算法时消除不必要的副本 [英] eliminate unnecessary copies when calling C++/STL algorithms

查看:166
本文介绍了在调用C ++ / STL算法时消除不必要的副本的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述


  • 我编写了以下示例,以更好地说明我的问题。


  • 在下面的代码中,我介绍了函数对象(即 funObj )。


  • funObj 类的定义是一个整数成员变量 id 定义为保存构造的每个 funObj 的I​​D以及静态整数成员变量 n funObj 创建的对象。


  • 因此,每次构建一个对象 funObj n 增加一并且其值被分配给新创建的 funObj id 字段。


  • 此外,我定义了一个默认构造函数,一个复制构造函数和一个析构函数。所有这三个都在 stdout 上打印信息以表示它们的调用以及它们所引用的 funObj 的I​​D至。


  • 我还定义了一个函数 func c $ c> funObj 。




/ h2>

  #include< vector> 
#include< iostream>
#include< algorithm>
#include< functional>

template< typename T>
class funObj {
std :: size_t id;
static std :: size_t n;
public:
funObj():id(++ n)
{
std :: cout< 通过默认构造函数构造,具有ID(<< id<<)< std :: endl;
}
funObj(funObj const& other):id(++ n)
{
std :: cout< 通过拷贝构造函数构造,具有ID(<< id<<)< std :: endl;
}
〜funObj()
{
std :: cout< Destroyed object foo with ID(<< id<<)< std :: endl;
}
void operator()(T& elem)
{

}
$ b return 1;
}
};

template< typename T>
void func(funObj< T> obj){obj(); }

template< typename T>
std :: size_t funObj< T> :: n = 0;

int main()
{
std :: vector< int> v {1,2,3,4,5,};
std :: cout<< >调用`func` ...< std :: endl;
func(funObj< int>());
std :: cout<< >调用`for_each` ...< std :: endl;
std :: for_each(std :: begin(v),std :: end(v),funObj< int>());
std :: cout<< >调用'generate` ...< std :: endl;
std :: generate(std :: begin(v),std :: end(v),funObj< int>());

// std :: ref
std :: cout<< >使用`std :: ref` ...<< std :: endl;
auto fobj1 = funObj< int>();
std :: cout<< >用`ref`调用`for_each` ...< std :: endl;
std :: for_each(std :: begin(v),std :: end(v),std :: ref(fobj1));
std :: cout<< >用`ref`调用`generate` ...< std :: endl;
std :: for_each(std :: begin(v),std :: end(v),std :: ref(fobj1));
return 0;
}



输出




调用 func ...


通过默认构造函数,ID为1的对象foo构造



使用ID(1)销毁对象foo


调用 for_each ...


通过默认构造函数构造,具有ID(2)的对象foo



通过拷贝构造函数构造,具有ID(3)的对象foo



使用ID(2)销毁对象foo



销毁ID为(3)的对象foo


调用生成 ...


通过默认构造函数构造,具有ID(4)的对象foo



通过拷贝构造函数构造,ID为5的对象foo



使用ID(5)销毁对象foo



销毁ID为4的对象foo


使用 std :: ref ...


通过默认构造函数构造,ID为6的对象foo


调用 for_each 与 ref ...



调用 / code>与 ref ...


销毁对象foo with ID(6)





讨论:
$ b

从上面的输出可以看出,使用类型 funObj func c $ c>导致构造单个 funObj 对象(即使 func 通过其参数传递其值)。但是,当将类型 funObj 的临时对象传递给STL算法 std :: for_each 时,似乎并非如此 std :: generate 。在前一种情况下,复制构造函数被调用,并构造一个额外的 funObj 。在相当多的应用中,这种不必要的副本的创建显着地恶化了算法的性能。基于这个事实,提出了以下问题。



问题:




  1. STL算法按值传递其参数。然而,与 func 相比,它还将其输入参数按值传递,因此STL算法生成额外的副本。

  2. 有没有办法消除这种不必要的副本?

  3. 当调用 std :: for_each(std :: begin(v),std :: end(v),funObj< int>()) func(funObj< int> ())其中范围分别对每个案例分别生成临时对象 funObj

  4. 我试图使用 std :: ref ,以强制传递通过引用,因为你可以看到不必要的副本被删除。然而,当我试图传递临时对象到 std :: ref (即 std :: ref(funObj< int> / code>)我得到一个编译器错误。为什么此类语句是非法的?

  5. 输出使用VC ++ 2013生成。正如你可以看到,当调用 std :: for_each 时,有一个异常,正在以相反的顺序调用对象的析构函数。为什么会这样?

  6. 当我在 Coliru上运行代码时运行GCC v4.8,具有析构函数的异常是固定的,但 std :: generate 不会生成额外的副本。为什么会这样?



详细信息/注释:




  • 上面的输出是从VC ++ 2013生成的。






  • 我也添加了 funObj class a move constructor (见下面的代码)。






  funObj (funObj&&& other):id(other.id)
{
other.id = 0;
std :: cout<< 通过移动构造函数构造,具有ID(<< id<<)< std :: endl;
}







  • 我还在VC ++ 2013中打开了完全优化并在发布模式下编译。



++ 2013):




调用 func ...


使用ID(1)的默认构造函数构造对象foo



销毁object foo with ID(1)


调用 for_each ...




通过默认构造函数构造,具有ID(2)的对象foo



移动构造函数,具有ID(2)的对象foo



销毁ID为(2)的对象foo



object foo with ID(0)


调用生成 ...




通过默认构造函数构造,具有ID(3)的对象foo



复制构造函数,带有ID(4)的对象foo



销毁ID为(4)的对象foo



object foo with ID(3)


使用 std :: ref .. 。


通过默认构造函数构造ID为5的对象foo


调用 for_each ref ...



使用 ref ...

调用生成


销毁ID为(5)的物件foo





输出GCC 4.8




调用 func


通过默认构造函数,ID为1的对象foo构造



使用ID(1)销毁对象foo


调用 for_each ...


通过默认构造函数构造,具有ID(2)的对象foo



通过移动构造函数构造,ID为2的对象foo



使用ID(2)销毁对象foo



用ID(0)销毁对象foo


调用生成 ...


使用ID(3)的默认构造函数构造对象foo



具有ID(3)的销毁对象foo



通过默认构造函数构造,具有ID(4)的对象foo


调用 for_each ref ...



使用 ref ...

调用生成


销毁ID为(4)的物件foo



看起来如果优化标志开启并且编译处于释放模式,并且除了移动的事实,VC ++ 2013 std :: generate 生成额外的复制无关构造函数。

解决方案


1 - 我知道大多数STL算法传递他们的参数按值。然而,与func相比,它也通过值传递其输入参数,STL算法生成额外的副本。


STL演算法会传回函式物件 。这发生,以致对象上的突变将是可观察的。您的 func 返回void,所以这是一个副本。




  • 很好,生成不会返回任何东西由 dyp)




2 - 是否有办法消除此类不必要的副本?


不必要有点太强了。函子的整个点是轻量级的对象,所以一个副本不重要。对于一种方式,你提供的(std :: ref)将做这项工作,唉,一个副本 std :: ref 将生成(你的对象赢了'



另一种方法是限定算法的调用



那么函数对象类型将是一个引用:

  auto fobj1 = funObj< int>(); 

std :: for_each< std :: vector< int> :: iterator,std :: vector< int> :: iterator,
funObj< int>& //这是魔法发生的地方!
(std :: begin(v),std :: end(v),fobj1);




3 - 当调用std :: for_each(std: :对于每种情况,对于每种情况,分别生成临时对象funObj的范围:begin(v),std :: end(v),funObj())和func(funObj())。


<$ c的正文$ c> std_for_each 扩展如下:

  template< class InputIterator,class函数> 
函数for_each(InputIterator first,InputIterator last,function fn)
{// 1
while(first!= last){
fn(* first);
++ first;
}
return fn; //或者,因为C ++ 11:return move(fn);
// 2
}

您的函数读为

 模板< typename T> 
void func(funObj< T> obj)
{// 1.
obj();
// 2.
}

注释 1 2 标记每种情况下的寿命。请注意,虽然如果应用了返回值优化(命名或未命名),编译器可能会生成将返回值(for_each中的函数对象)放置在调用者的堆栈框架中的代码,寿命更长。


4 - 我试图使用std :: ref以强制传递引用,看到不必要的副本被删除。然而,当我尝试传递临时对象到std :: ref(即,std :: ref(funObj()))我得到一个编译器错误。为什么此类语句是非法的?


std :: ref 不能使用r值引用(STL代码如下):

  template< class _Ty> 
void ref(const _Ty&&)= delete;

您需要传递l值



< blockquote>

5 - 输出使用VC ++ 2013生成。正如你可以看到,有一个异常,当调用std :: for_each时,对象的析构函数以相反的顺序被调用。为什么会这样?



6 - 当我运行在运行GCC v4.8的Coliru代码时,析构函数的异常是固定的, :: generate不会生成额外的副本。为什么会这样?





  • 检查每个编辑的设置。通过优化ON(以及VS的发布)复制删除/消除额外副本/忽略不可观察的行为是可能的。


  • 其次(在我看来)在VS 2013中的函数 for_each generate 都是通过值传递的(没有签名接受r值引用),因此这很明显是 copy




重要的是, gcc中的STL实现也没有接受r值引用的签名(请

 模板< typename _InputIterator,typename _Function> 
_Function
for_each(_InputIterator __first,_InputIterator __last,_Function __f)
{
//概念要求
__glibcxx_function_requires(_InputIteratorConcept< _InputIterator>)
__glibcxx_requires_valid_range __first,__last);
for(; __first!= __last; ++ __ first)
__f(* __ first);
return _GLIBCXX_MOVE(__ f);
}

所以我可能会走出这一边的肢体,你的函子的移动语义没有效果,只有编译器优化适用于消除副本


  • I've coded the following example in order to better illustrate my questions.

  • In the code below, I introduce a function object (i.e., funObj).

  • In funObj class's definition an integral member variable called id is defined to hold the ID of every funObj constructed and a static integral member variable n to count the funObj objects created.

  • Thus, every time an object funObj is constructed n is increased by one and its value is assigned to the id field of the newly created funObj.

  • Furthermore, I've defined a default constructor, a copy constructor and a destructor. All three are printing messages to the stdout in order to signify their invocation along with the ID of the funObj they are referring to.

  • I've also defined a function func that takes as inputs by value objects of type funObj.

Code:

#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

template<typename T>
class funObj {
  std::size_t id;
  static std::size_t n;
public:
  funObj() : id(++n) 
  { 
    std::cout << "    Constructed via the default constructor, object foo with ID(" << id << ")" << std::endl;
  }
  funObj(funObj const &other) : id(++n) 
  {
    std::cout << "    Constructed via the copy constructor, object foo with ID(" << id << ")" << std::endl;
  }
  ~funObj()
  { 
    std::cout << "    Destroyed object foo with ID(" << id << ")" << std::endl;
  }
  void operator()(T &elem)
  { 

  }
  T operator()()
  {
    return 1;
  }
};

template<typename T>
void func(funObj<T> obj) { obj();  }

template<typename T>
std::size_t funObj<T>::n = 0;

int main()
{
  std::vector<int> v{ 1, 2, 3, 4, 5, };
  std::cout << "> Calling `func`..." << std::endl;
  func(funObj<int>());
  std::cout << "> Calling `for_each`..." << std::endl;
  std::for_each(std::begin(v), std::end(v), funObj<int>());
  std::cout << "> Calling `generate`..." << std::endl;
  std::generate(std::begin(v), std::end(v), funObj<int>());

  // std::ref 
  std::cout << "> Using `std::ref`..." << std::endl;
  auto fobj1 = funObj<int>();
  std::cout << "> Calling `for_each` with `ref`..." << std::endl;
  std::for_each(std::begin(v), std::end(v), std::ref(fobj1));
  std::cout << "> Calling `generate` with `ref`..." << std::endl;
  std::for_each(std::begin(v), std::end(v), std::ref(fobj1));
  return 0;
}

Output:

Calling func...

Constructed via the default constructor, object foo with ID(1)

Destroyed object foo with ID(1)

Calling for_each...

Constructed via the default constructor, object foo with ID(2)

Constructed via the copy constructor, object foo with ID(3)

Destroyed object foo with ID(2)

Destroyed object foo with ID(3)

Calling generate...

Constructed via the default constructor, object foo with ID(4)

Constructed via the copy constructor, object foo with ID(5)

Destroyed object foo with ID(5)

Destroyed object foo with ID(4)

Using std::ref...

Constructed via the default constructor, object foo with ID(6)

Calling for_each with ref...

Calling generate with ref...

Destroyed object foo with ID(6)

Discussion:

As you can see from the output above, calling function func with a temporary object of type funObj results in the construction of a single funObj object (even though func passes its argument by value). However, this seems not to be the case when passing temporary objects of type funObj to STL algorithms std::for_each and std::generate. In the former cases the copy constructor is evoked and an extra funObj is constructed. In quite a few applications the creation of such "unnecessary" copies deteriorates the performance of the algorithm significantly. Based on this fact the following questions are being raised.

Questions:

  1. I know that most STL algorithms pass their argument by value. However, compared to func, that also passes its input argument by value, the STL algorithms generate an extra copy. What's the reason for this "unnecessary" copy?
  2. Is there a way to eliminate such "unnecessary" copies?
  3. When calling std::for_each(std::begin(v), std::end(v), funObj<int>()) and func(funObj<int>()) in which scope does temporary object funObj<int> lives, for each case respectively?
  4. I've tried to use std::ref in order to force pass by reference and as you can see the "unnecessary" copy was eliminated. However, when I try to pass a temporary object to std::ref (i.e., std::ref(funObj<int>())) I get a compiler error. Why such kind of statements are illegal?
  5. The output was generated using VC++2013. As you can see there's an anomaly when calling std::for_each the destructors of the objects are being called in reversed order. Why is that so?
  6. When I run the code on Coliru that runs GCC v4.8 the anomaly with destructors is fixed however std::generate doesn't generate an extra copy. Why is that so?

Details/Comments:

  • The output above was generated from VC++2013.

Update:

  • I've also added to funObj class a move constructor (see code below).

 funObj(funObj&& other) : id(other.id)
  {
    other.id = 0;
    std::cout << "    Constructed via the move constructor, object foo with ID(" << id << ")" << std::endl;
  }


  • I've also turned on full optimization in VC++2013 and compiled in release mode.

Output (VC++2013):

Calling func...

Constructed via the default constructor, object foo with ID(1)

Destroyed object foo with ID(1)

Calling for_each...

Constructed via the default constructor, object foo with ID(2)

Constructed via the move constructor, object foo with ID(2)

Destroyed object foo with ID(2)

Destroyed object foo with ID(0)

Calling generate...

Constructed via the default constructor, object foo with ID(3)

Constructed via the copy constructor, object foo with ID(4)

Destroyed object foo with ID(4)

Destroyed object foo with ID(3)

Using std::ref...

Constructed via the default constructor, object foo with ID(5)

Calling for_each with ref...

Calling generate with ref...

Destroyed object foo with ID(5)

Output GCC 4.8

Calling func...

Constructed via the default constructor, object foo with ID(1)

Destroyed object foo with ID(1)

Calling for_each...

Constructed via the default constructor, object foo with ID(2)

Constructed via the move constructor, object foo with ID(2)

Destroyed object foo with ID(2)

Destroyed object foo with ID(0)

Calling generate...

Constructed via the default constructor, object foo with ID(3)

Destroyed object foo with ID(3)

Constructed via the default constructor, object foo with ID(4)

Calling for_each with ref...

Calling generate with ref...

Destroyed object foo with ID(4)

It seems that VC++2013 std::generate generates an extra copy no-matter if optimization flags are on and compilation is in release mode and besides the fact that a move constructor is defined.

解决方案

1 - I know that most STL algorithms pass their argument by value. However, compared to func, that also passes its input argument by value, the STL algorithms generate an extra copy. What's the reason for this "unnecessary" copy?

STL algorithms return the function object. This happens so that the mutation on the object will be observable. Your func returns void so that's a copy less.

  • Well, to be precise, generate does not return a thing (see comment by dyp)

2 - Is there a way to eliminate such "unnecessary" copies?

Well unnecessary is a bit too strong. The whole point of functors is to be lightweight objects so that a copy wouldn't matter. As for a way, the one you provide (std::ref) will do the job, alas a copy of the std::ref will be generated (your object won't get copied though)

Another way would be to qualify the call of the algorithm

then the function object type will be a reference :

auto fobj1 = funObj<int>();

std::for_each<std::vector<int>::iterator, std::vector<int>::iterator, 
funObj<int>&> // this is where the magic happens !!
(std::begin(v), std::end(v), fobj1);

3 - When calling std::for_each(std::begin(v), std::end(v), funObj()) and func(funObj()) in which scope does temporary object funObj lives, for each case respectively?

The body of std_for_each is expanded as follows :

template<class InputIterator, class Function>
  Function for_each(InputIterator first, InputIterator last, Function fn)
{ // 1
  while (first!=last) {
    fn (*first);
    ++first;
  }
  return fn;      // or, since C++11: return move(fn);
// 2
}

your function reads

template<typename T>
void func(funObj<T> obj) 
{ // 1.
    obj();  
// 2.
}

The comments 1 and 2 mark the lifespan in each case. Note though that if a return value optimization applies (named or unnamed), then the compiler may generate code that places the return value (the function object in for_each) in the stack frame of the caller, so the life span is longer.

4 - I've tried to use std::ref in order to force pass-by-reference and as you can see the "unnecessary" copy was eliminated. However, when I try to pass a temporary object to std::ref (i.e., std::ref(funObj())) I get a compiler error. Why such kind of statements are illegal?

std::ref does not work with r-value references (STL code follows):

template<class _Ty>
void ref(const _Ty&&) = delete;

you need to pass an l-value

5 - The output was generated using VC++2013. As you can see there's an anomaly when calling std::for_each the destructors of the objects are being called in reversed order. Why is that so?

6 - When I run the code on Coliru that runs GCC v4.8 the anomaly with destructors is fixed however std::generate doesn't generate an extra copy. Why is that so?

  • Check the settings for each compilation. With optimizations ON (and in Release for VS) copy elision / elimination of extra copies / ignoring non observable behaviors, are possible.

  • Secondly (as far as I can see) in VS 2013 the functor in for_each and the generator in generate are both passed by value (there's no signature accepting an r-value reference) so it would be clearly a matter of copy elision to save the extra copy.

For what matters, the STL implementation in gcc also has no signatures that accept r-value references (please notify me if one having is spotted)

template<typename _InputIterator, typename _Function>
_Function
for_each(_InputIterator __first, _InputIterator __last, _Function __f)
{
  // concept requirements
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
  __glibcxx_requires_valid_range(__first, __last);
  for (; __first != __last; ++__first)
__f(*__first);
  return _GLIBCXX_MOVE(__f);
}

so I may be going out on limb on this one and assume, that defining move semantics for your functor has no effect and only compiler optimizations apply to eliminate copies

这篇关于在调用C ++ / STL算法时消除不必要的副本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆