c++ - 调用C++/STL算法时消除不必要的拷贝

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

  • 我已编码以下示例,以便更好地说明我的问题。
  • 在下面的代码中,我介绍了function object(即funObj)。
  • funObj类的定义中,定义了一个称为id的积分成员变量以保存所构造的每个funObj的ID,并定义了一个静态积分成员变量n来计数创建的funObj对象。
  • 因此,每次构造一个对象funObj时,n都会增加1,并将其值分配给新创建的idfunObj字段。
  • 此外,我定义了一个默认构造函数,一个复制构造函数和一个析构函数。这三者都将消息打印到stdout上,以表示它们的调用以及它们所引用的funObj的ID。
  • 我还定义了一个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;
    }
    

    输出:

    Calling func...

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

    Destroyed object foo with ID(1)



    调用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)



    调用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)



    使用std::ref ...

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



    for_each调用ref ...

    generate调用ref ...

    Destroyed object foo with ID(6)



    讨论:

    从上面的输出中可以看到,使用func类型的临时对象调用函数funObj会导致构造单个funObj对象(即使func通过值传递其参数)。但是,当将funObj类型的临时对象传递给STL算法std::for_eachstd::generate时,情况似乎并非如此。在前一种情况下,将引发复制构造函数并构造一个额外的funObj。在许多应用中,创建这种“不必要的”拷贝会大大降低算法的性能。基于这一事实,提出了以下问题。

    问题:
  • 我知道大多数STL算法都按值传递其参数。但是,与func(也按值传递其输入参数)相比,STL算法会生成一个额外的拷贝。此“不必要”拷贝的原因是什么?
  • 是否有消除这种“不必要”拷贝的方法?
  • 在每种情况下分别调用std::for_each(std::begin(v), std::end(v), funObj<int>())func(funObj<int>())时,临时对象funObj<int>驻留在哪个范围内?
  • 我尝试使用std::ref来强制通过引用传递,并且如您所见,消除了“不必要的”拷贝。但是,当我尝试将临时对象传递给std::ref(即std::ref(funObj<int>()))时,出现编译器错误。为什么这种陈述是非法的?
  • 使用VC++ 2013生成了输出。如您所见,调用std::for_each时存在异常,以相反的顺序调用对象的析构函数。为什么会这样?
  • 当我在运行GCC v4.8的Coliru上运行代码时,析构函数异常已修复,但是std::generate不会生成额外的拷贝。为什么会这样?

  • 详细信息/评论:
  • 上面的输出是从VC++ 2013生成的。

  • 更新:
  • 我还向funObj类添加了一个move构造函数(请参见下面的代码)。

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

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

  • 输出(VC++ 2013):

    Calling func...

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

    Destroyed object foo with ID(1)



    调用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)



    调用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)



    使用std::ref ...

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



    for_each调用ref ...

    generate调用ref ...

    Destroyed object foo with ID(5)



    输出GCC 4.8

    Calling func...

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

    Destroyed object foo with ID(1)



    调用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)



    调用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)



    for_each调用ref ...

    generate调用ref ...

    Destroyed object foo with ID(4)



    似乎VC++ 2013 std::generate如果启用了优化标志且编译处于 Release模式,并且定义了move构造函数,则将生成一个额外的拷贝。

    最佳答案

    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算法返回函数对象。发生这种情况,以便可以观察到对象上的突变。您的func返回void,因此少了一个拷贝。
  • 好吧,确切地说,generate不返回任何东西(请参阅dyp)的评论

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



    那么不必要的有点太强大了。仿函数的重点是轻量级对象,因此复制无关紧要。至于一种方式,您提供的(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 - 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?



    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 - 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不适用于r值引用(后接STL代码):
    template<class _Ty>
    void ref(const _Ty&&) = delete;
    

    您需要传递一个L值

    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?


  • 检查每个编译的设置。通过启用优化(以及在VS中发行),可以实现复制省略/消除多余的拷贝/忽略不可观察的行为。
  • 其次(据我所知)在VS 2013中,for_each中的仿函数和generate中的生成器均按值传递(,没有签名接受r值引用),因此显然,这是复制的问题elision 保存多余的拷贝。

  • 重要的是,STL implementation in gcc也没有接受r值引用的签名(如果发现有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);
    }
    

    因此,我可能会对此一无所知,并假设为您的仿函数定义移动语义没有任何效果,只有编译器优化可用于消除拷贝

    关于c++ - 调用C++/STL算法时消除不必要的拷贝,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23613574/

    相关文章:

    c++ - 具有两个 cpp_int 值的 boost::multiprecision::pow

    c++ - 具有 std::vector 和 std::set 属性的容器?

    c++ - 模板类重写基类虚函数

    c++ - 为什么这个简单函数的编译器输出如此不同?

    c++ - 在MFC图片框中显示IPLimage/MAT(opencv)

    c++ - "extern __forceinline "是什么 C++ 习语?

    C++ access_once

    c++ - 访问集合中的元素?

    c++ - MFC 日期时间选取器零值

    c++ - ABI稳定性: When do I break ABI?