在调用C++/STL算法时,可视化消除了不必要的副本
visual eliminate unnecessary copies when calling C++/STL algorithms
-
为了更好地说明我的问题,我编写了以下示例。
-
在下面的代码中,我介绍了一个函数对象(即
funObj
)。 -
在
funObj
类的定义中,定义了一个名为id
的积分成员变量来保存所构建的每个funObj
的ID,并定义了一种静态积分成员变量n
来计数所创建的funObj
对象。 -
因此,每次构造对象
funObj
时,n
增加一,并且其值被分配给新创建的funObj
的id
字段。 -
此外,我还定义了一个默认构造函数、一个复制构造函数和一个析构函数。这三个都在向
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_each
和std::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中启用了完全优化,并在发布模式下编译
输出(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算法,而不会在性能方面牺牲太多。它还允许您根据需要使用临时功能对象。
- 用callgrind追踪不必要的副本
- 不必要的C++代码最终会出现在我完成的程序中吗?
- 总和的不必要行为C++?
- C++:将初始化的对象传递给另一个类的构造函数;需要不必要的构造函数吗?
- 为什么 std::string s = "123" 当不涉及副本时被视为复制初始化?
- 在这种情况下,使用 string_view 是否会导致不必要的字符串复制?
- std::mutex::lock() 产生奇怪(和不必要的)ASM 代码
- 如何在插入排序中使用 replace() 使语句变得不必要
- C 包装器C++库周围没有不必要的头文件
- 编译器是否消除了不必要的原子?
- 在 c++ 中不必要的包含
- GCC为AVR上的简单ISR产生不必要的寄存器推送
- 在序列化过程中删除不必要的内存分配
- QTREEWIDGET子分类,停止下降指示器显示给定有不必要的DropIndicatorPosition
- 如何在不指定不必要的模板参数的情况下使用模板类的成员类型
- 构造向量时如何摆脱(一个)不必要的副本
- 处理对“vector_binary_operation”类中“表达式”的引用,而无需不必要的副本
- 在调用C++/STL算法时,可视化消除了不必要的副本
- 在构建复合对象时消除不必要的副本
- 在汇编代码中查找不必要的缓冲区副本