在调用C ++ / STL算法时消除不必要的副本

时间:2014-05-12 15:54:43

标签: c++ visual-c++ c++11 stl g++4.8

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

  • 在下面的代码中,我介绍了function object(即funObj)。

  • funObj类的定义中,定义了一个名为id的整数成员变量,用于保存构造的每个funObj的ID和一个静态整数成员变量{{ 1}}计算创建的n个对象。

  • 因此,每次构建对象funObj时,funObj都会增加1,其值将分配给新创建的n id字段。

  • 此外,我已经定义了默认构造函数,复制构造函数和析构函数。这三个人都在向funObj打印消息,以便表明他们的调用以及他们所指的stdout的ID。

  • 我还定义了一个函数funObj,它被func类型的值对象作为输入。

代码:

funObj

输出:

  

致电#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; } ...

     
    

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

         

使用ID(1)

销毁对象foo   
     

致电func ...

     
    

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

         

通过复制构造函数构造,对象foo为ID(3)

         

使用ID(2)

销毁对象foo          

使用ID(3)

销毁对象foo   
     

致电for_each ...

     
    

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

         

通过复制构造函数构造,ID为对象foo(5)

         

使用ID(5)

销毁对象foo          

使用ID(4)

销毁对象foo   
     

使用generate ...

     
    

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

  
     

使用std::ref ...

致电for_each      

使用ref ...

致电generate      
    

使用ID(6)

销毁对象foo   

讨论:

从上面的输出中可以看出,使用类型为ref的临时对象调用函数func会导致构造单个funObj对象(即使funObj }通过值传递其参数)。但是,将func类型的临时对象传递给STL算法funObjstd::for_each时似乎并非如此。在前一种情况下,引发了复制构造函数,并构造了一个额外的std::generate。在很多应用程序中创建了这样的&#34;不必要的&#34;副本显着降低了算法的性能。基于这一事实,正在提出以下问题。

问题:

  1. 我知道大多数STL算法都是按值传递参数。但是,与funObj相比,它还通过值传递其输入参数,STL算法生成一个额外的副本。这是什么原因以及不必要的&#34;副本?
  2. 有没有办法消除这种&#34;不必要的&#34;副本?
  3. 分别针对每个案例调用funcstd::for_each(std::begin(v), std::end(v), funObj<int>())范围内的临时对象func(funObj<int>())
  4. 我尝试使用funObj<int>强制通过引用传递,因为您可以看到&#34;不必要的&#34;副本被删除了。但是,当我尝试将临时对象传递给std::ref(即std::ref)时,我收到编译器错误。为什么这种陈述是非法的?
  5. 使用VC ++ 2013生成输出。正如您所看到的那样,在调用std::ref(funObj<int>())时,异常会以相反的顺序调用对象的析构函数。为什么会这样?
  6. 当我在运行GCC v4.8的Coliru上运行代码时,修复了析构函数的异常,但std::for_each没有生成额外的副本。为什么会这样?
  7. 详情/评论:

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

    更新

    • 我还在std::generate类中添加了一个移动构造函数(请参阅下面的代码)。

    funObj

    • 我还在VC ++ 2013中启用了完全优化,并在发布模式下进行了编译。

    输出(VC ++ 2013):

      

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

         
        

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

             

    使用ID(1)

    销毁对象foo   
         

    致电func ...

         
        

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

             

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

             

    使用ID(2)

    销毁对象foo          

    使用ID(0)

    销毁对象foo   
         

    致电for_each ...

         
        

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

             

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

             

    使用ID(4)

    销毁对象foo          

    使用ID(3)

    销毁对象foo   
         

    使用generate ...

         
        

    通过默认构造函数构造,对象foo为ID(5)

      
         

    使用std::ref ...

    致电for_each      

    使用ref ...

    致电generate      
        

    使用ID(5)

    销毁对象foo   

    输出GCC 4.8

      

    致电ref ...

         
        

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

             

    使用ID(1)

    销毁对象foo   
         

    致电func ...

         
        

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

             

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

             

    使用ID(2)

    销毁对象foo          

    使用ID(0)

    销毁对象foo   
         

    致电for_each ...

         
        

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

             

    使用ID(3)

    销毁对象foo          

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

      
         

    使用generate ...

    致电for_each      

    使用ref ...

    致电generate      
        

    使用ID(4)

    销毁对象foo   

    如果启用了优化标志并且编译处于发布模式,并且除了定义了移动构造函数之外,VC ++ 2013 ref似乎生成了一个额外的副本。

2 个答案:

答案 0 :(得分:4)

  

1 - 我知道大多数STL算法都是按值传递参数。但是,与func相比,它还通过值传递其输入参数,STL算法生成一个额外的副本。这个“不必要”的副本是什么原因?

STL算法返回函数对象。发生这种情况,以便可以观察到对象上的突变。您的func返回无效,因此副本较少。

  • 嗯,确切地说,generate不会返回任何内容(请参阅dyp)的评论
  

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

不必要有点太强了。仿函数的重点是轻量级对象,因此副本无关紧要。至于一种方式,你提供的那个(std :: ref)将完成这项工作,唉将生成std::ref的副本(你的对象不会被复制)

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

然后函数对象类型将是一个引用:

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 - 调用std :: for_each(std :: begin(v),std :: end(v),funObj())和func(funObj()),其中范围是临时对象funObj生活,分别针对每个案例?

std_for_each的正文扩展如下:

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
}

你的函数读取

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

评论12标志着每种情况下的生命周期。请注意,如果返回值优化应用 (命名或未命名),则编译器可能会生成将返回值(for_each中的函数对象)放在调用者的堆栈帧中的代码,因此寿命更长。

  

4 - 我试图使用std :: ref来强制传递引用,因为你可以看到“不必要的”副本被删除了。但是,当我尝试将临时对象传递给std :: ref(即std :: ref(funObj()))时,我收到编译器错误。为什么这种陈述是非法的?

std::ref不适用于r值引用(后面是STL代码):

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

你需要传递一个l值

  

5 - 使用VC ++ 2013生成输出。正如您所看到的,在调用std :: for_each时会出现异常,对象的析构函数将以相反的顺序调用。为什么会这样?

     

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

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

  • 其次(据我所知)在VS 2013中,for_each中的仿函数和generate中的生成器都按值传递(没有签名接受r - 值参考)因此显然需要复制省略来保存额外的副本。

重要的是,STL implementation in gcc也没有接受r值引用的签名(如果有人被发现,请通知我)

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);
}

因此我可能会对此问题采取行动并假设,为您的仿函数定义移动语义没有任何效果,只有编译器优化适用于消除副本

答案 1 :(得分:3)

C ++ 11中引入的移动语义存在很大程度上缓解了这组“不必要”的副本。如果为函数对象定义move constructor,STL将move函数对象(偶数/特别是如果它是临时的)将阻止副本发生。这将允许您使用具有值语义的STL算法,而不会牺牲太多的性能。它还允许您根据需要使用临时函数对象。