在调用C++/STL算法时,可视化消除了不必要的副本

visual eliminate unnecessary copies when calling C++/STL algorithms

本文关键字:不必要 副本 可视化 C++ 调用 STL 算法      更新时间:2023-10-16
  • 为了更好地说明我的问题,我编写了以下示例。

  • 在下面的代码中,我介绍了一个函数对象(即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。。。

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

ID为(1)的已销毁对象foo

正在调用for_each。。。

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

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

ID为(2)的已销毁对象foo

ID为(3)的已销毁对象foo

正在调用generate。。。

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

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

ID为(5)的已销毁对象foo

ID为(4)的已销毁对象foo

使用std::ref。。。

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

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<int>())func(funObj<int>())时,临时对象funObj<int>分别生活在哪个作用域中
  4. 我已经尝试使用std::ref来强制通过引用,正如你所看到的,"不必要的"拷贝被消除了。然而,当我试图将一个临时对象传递给std::ref(即std::ref(funObj<int>()))时,我会遇到编译器错误。为什么这种言论是非法的
  5. 输出是使用VC++2013生成的。正如您所看到的,在调用std::for_each时出现异常,对象的析构函数被以相反的顺序调用。为什么会这样
  6. 当我在运行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中启用了完全优化,并在发布模式下编译

输出(VC++2013):

正在调用func。。。

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

ID为(1)的已销毁对象foo

正在调用for_each。。。

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

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

ID为(2)的已销毁对象foo

已销毁ID为(0)的对象foo

正在调用generate。。。

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

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

ID为(4)的已销毁对象foo

ID为(3)的已销毁对象foo

使用std::ref。。。

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

ref调用for_each。。。

ref调用generate。。。

已销毁ID为(5)的对象foo

输出GCC 4.8

正在调用func。。。

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

ID为(1)的已销毁对象foo

正在调用for_each。。。

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

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

ID为(2)的已销毁对象foo

已销毁ID为(0)的对象foo

正在调用generate。。。

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

ID为(3)的已销毁对象foo

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

ref调用for_each。。。

ref调用generate。。。

已销毁ID为(4)的对象foo

似乎VC++2013std::generate生成了一个额外的副本,无论优化标志是否打开,编译是否处于发布模式,以及是否定义了移动构造函数。

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.
}

注释CCD_ 61和CCD_。请注意,如果应用返回值优化(命名或未命名),则编译器可能会生成将返回值(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不会生成额外的副本。为什么会这样

  • 检查每次编译的设置。启用优化后(在VS版本中),可以省略/消除额外副本/忽略不可观察的行为。

  • 其次(据我所见)在VS 2013中,for_each中的函子和generate中的生成器都是按值传递的(没有接受r值引用的签名),因此保存额外的副本显然是副本省略的问题。

重要的是,gcc中的STL实现也没有接受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++11中引入的移动语义在很大程度上减少了这组"不必要"的副本。如果您为函数对象定义了move constructor,STL将move函数对象(即使/特别是如果它是临时对象),这将防止复制发生。这将允许您使用具有值语义的STL算法,而不会在性能方面牺牲太多。它还允许您根据需要使用临时功能对象。