【问题标题】:eliminate unnecessary copies when calling C++/STL algorithms在调用 C++/STL 算法时消除不必要的副本
【发布时间】:2014-06-30 02:00:35
【问题描述】:
  • 为了更好地说明我的问题,我编写了以下示例。

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

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

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

  • 此外,我还定义了一个默认构造函数、一个复制构造函数和一个析构函数。这三个人都在向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;
}

输出:

致电func...

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

ID(1) 的销毁对象 foo

致电for_each...

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

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

ID(2) 的销毁对象 foo

ID(3) 的销毁对象 foo

致电generate...

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

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

ID(5) 的销毁对象 foo

ID(4) 的销毁对象 foo

使用std::ref...

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

ref 呼叫for_each...

ref调用generate...

ID(6) 的销毁对象 foo

讨论:

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

问题:

  1. 我知道大多数 STL 算法通过值传递它们的参数。但是,与同样按值传递其输入参数的func 相比,STL 算法会生成一个额外的副本。这种“不必要的”副本的原因是什么?
  2. 有没有办法消除这种“不必要的”副本?
  3. 在调用std::for_each(std::begin(v), std::end(v), funObj&lt;int&gt;())func(funObj&lt;int&gt;()) 时,临时对象funObj&lt;int&gt; 分别存在于哪个范围内?
  4. 我尝试使用std::ref 来强制通过引用传递,您可以看到“不必要的”副本已被删除。但是,当我尝试将临时对象传递给std::ref(即std::ref(funObj&lt;int&gt;()))时,我收到编译器错误。为什么这样的陈述是非法的?
  5. 输出是使用 VC++2013 生成的。如您所见,调用std::for_each 时出现异常,对象的析构函数以相反的顺序调用。为什么会这样?
  6. 当我在运行 GCC v4.8 的 Coliru 上运行代码时,带有析构函数的异常已修复,但 std::generate 不会生成额外的副本。为什么会这样?

详情/评论:

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

更新:

  • 我还在funObj 类中添加了一个移动构造函数(参见下面的代码)。

 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 中开启了全面优化,并在发布模式下编译。

输出(VC++2013):

打电话给func...

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

ID(1) 的销毁对象 foo

打电话给for_each...

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

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

ID(2) 的销毁对象 foo

已销毁 ID(0) 的对象 foo

打电话给generate...

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

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

ID(4) 的销毁对象 foo

ID(3) 的销毁对象 foo

使用std::ref...

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

ref 呼叫for_each...

ref 呼叫generate...

ID(5) 的销毁对象 foo

输出 GCC 4.8

致电func...

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

ID(1) 的销毁对象 foo

致电for_each...

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

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

ID(2) 的销毁对象 foo

已销毁 ID(0) 的对象 foo

致电generate...

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

ID(3) 的销毁对象 foo

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

ref 呼叫for_each...

ref 呼叫generate...

ID(4) 的销毁对象 foo

似乎 VC++2013 std::generate 如果优化标志打开并且编译处于发布模式并且除了定义了移动构造函数这一事实之外,还会生成额外的副本。

【问题讨论】:

  • 总结大部分内容,我想您只想问“为什么 STL 采用值谓词?”
  • 通过定义复制构造函数,您可以防止编译器生成隐式定义的移动构造函数。您可以通过定义自己的移动构造函数来阻止大部分副本。
  • FWIW VS2013 无论如何都不会生成移动构造函数。你总是需要提供你自己的。玩得开心。
  • @40two - 您需要在打开优化的情况下进行编译。否则,您将运行“已检查但速度较慢”的调试版本。来自调试版本的统计数据并不是编译器真正可以生成的代码的真实画面。
  • 如果你为你的函数对象类定义一个移动构造函数会发生什么?如果 STL 实现者正确地完成了他们的工作,那应该会消除 STL 算法中的额外副本。

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


【解决方案1】:

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

【讨论】:

  • 如果没有提供用户定义的复制或移动构造函数或复制赋值运算符,大多数编译器将生成隐式移动构造函数。在 VC++ 2013 上可能不是这种情况。
  • 但是这里的问题是有一个用户定义的复制构造函数,因此,移动构造函数被隐式删除。这就是为什么这里需要一个用户定义的移动构造函数。
【解决方案2】:

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

STL 算法返回函数对象。发生这种情况是为了使对象上的突变是可观察的。你的 func 返回 void 所以这是一个副本少。

  • 嗯,准确地说,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.
}

cmets 12 在每种情况下都标记了使用寿命。请注意,虽然如果应用了返回值优化(命名或未命名),那么编译器可能会生成将返回值(for_each 中的函数对象)放入调用者的堆栈帧的代码,因此寿命更长。

4 - 我尝试使用 std::ref 来强制传递引用,如您所见,“不必要的”副本已被消除。但是,当我尝试将临时对象传递给 std::ref(即 std::ref(funObj()))时,会出现编译器错误。为什么这种说法是非法的?

std::ref 不适用于右值引用(STL 代码如下):

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

你需要传递一个左值

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

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

  • 检查每个编译的设置。通过优化开启(以及在 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);
}

所以我可能会在这个问题上犹豫不决并假设,为你的仿函数定义移动语义没有任何效果,只有编译器优化适用于消除副本

【讨论】:

  • // or, since C++11: return move(fn); 你不应该在这里显式地移动到返回值,它是由编译器自动完成的。仅当它不是局部变量/参数,或者返回表达式的类型与返回类型不同时。
  • @dyp 在// or, since C++11: return move(fn); 上是来自cplusplus.com 的复制粘贴
  • 嗯...删除了该评论,因为我认为它是错误的。事实证明它毕竟是正确的:"请注意,如果应用了返回值优化(命名或未命名),那么编译器可能会生成将返回值(for_each 中的函数对象)放置在堆栈帧中的代码调用者” RVO 不适用于函数参数。临时对象的生命周期不依赖于它被传递到的函数,它只会一直存在到创建它的完整表达式的末尾,除非通过将其绑定到引用来延长它的生命周期。跨度>
  • @dyp 我只是在回复,你让我加班,30 秒内 3 分!我添加了引用
  • 总得有人让你忙;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多