移动构造函数(浅拷贝)和具名返回值优化(NRVO)
Ruichen Ni
需求来源
最近笔者在给UI前端撰写一个简单的数学库,满足一些简单常用的矩阵操作,例如基本的加减乘除、线性方程组(\(A\cdot x=b\))的求解,基于SVD的矩阵特征值求解等等。
一开始考虑过采用LAPACK形式的函数接口,以LAPACK库中求解最小二乘法系数的函数为例:
lapack_int LAPACKE_dgels(int matrix_layout, char trans, lapack_int m,
lapack_int n, lapack_int nrhs, double* a,
lapack_int lda, double* b, lapack_int ldb);
LAPACK库为了节约内存,将计算结果储存在输入的(double*) b
中,并且根据问题形状(m
和n
)的大小关系,(double*) b
对计算结果的储存形式也会发生变化。和UI前端的技术人员聊过之后,他们一致觉得最好不要让他们再来考虑输入输出的存储形式,并且对函数的调用形式越接近MATLAB
的方式越好。
还是以求解最小二乘法系数为例,初步的接口形式定义如下:
// Matrix is a to-be-implemented class for matrix data storage
// The Least-Squared problem of Ax=b is (A^T)Ax=(A^T)b.
// The return value contains the coefficient result.
Matrix MATH_PACK_GELS(const Matrix& A, const Matrix& b);
那么函数内的临时对象作为返回值返回时就需要关注大数据的拷贝操作,从而避免运算效率的降低。
具名返回值优化(NRVO)
关于编译器的返回值优化(RVO)和具名返回值优化(NRVO)可以参见这篇文章。这里我们简单摘录一下这两个名词所代表的含义:
- 返回值优化(RVO): RVO是指在函数中直接构造返回值到调用函数的返回位置的优化。也就是说,编译器会将函数的返回值直接在调用者的上下文中构造,从而避免了复制或移动构造函数的调用。RVO最常见的情况是当函数返回一个临时对象时。
- 具名返回值优化(NRVO): NRVO则是RVO的一个特殊情况,它适用于当函数返回一个具名的局部对象时。编译器会尝试消除这个局部对象和接收对象之间的复制或移动操作。
Tips: 经后续小节测试,当我开启
Debug
模式时,编译器还是会在退出函数体时调用构造函数(优先移动构造函数,其次复制构造函数);当开启Release
模式时(编译选项’/O2’),msvc
的编译器会进行返回值优化不再调用构造函数。
移动构造函数
为了避免Debug
模式下或编译器不支持具名返回值优化功能时的大数据拷贝操作,还是推荐增加移动构造函数来实现对数据的浅拷贝。以一个简单的类定义为例:
// "MyClass.h"
class MyClass
{
public:
/// Default constructor
MyClass() {
array_ = new double[4];
array_[0] = 1;
array_[1] = 2;
array_[2] = 3;
array_[3] = 4;
}
/// Copy constructor
MyClass(const MyClass& other) {
cout << "Copy Constructor" << endl;
array_ = new double[4];
for (size_t i = 0; i < 4; i++)
array_[i] = other.array_[i];
}
/// Move constructor
MyClass(MyClass&& other) {
cout << "Move Constructor" << endl;
array_ = other.array_;
other.array_ = nullptr;
}
/// Deconstructor
~MyClass() {
cout << "De-Constructor" << endl;
if (array_) delete[] array_;
}
/// Member parameter
double* array_;
};
具名返回值优化(NRVO)测试
测试代码
以下是对具名返回值优化(NRVO)功能测试的主程序代码:
#include "MyClass.h"
MyClass getObject()
{
MyClass new_object = MyClass();
cout << new_object.array_ << endl; /// To see if the array_ pointer is shallow copied
return new_object;
}
int main()
{
MyClass object = getObject();
cout << object.array_ << endl;
return 0;
}
Release模式
Console打印结果如下(注意:具体的数组首地址结果可能不一致):
000002093D262540
000002093D262540
De-Constructor
Debug模式
Console打印结果如下:
00000213991A23A0
Move Constructor
De-Constructor
00000213991A23A0
De-Constructor
可以看到,在Debug
模式下返回内部临时变量时会调用构造函数(优先移动构造函数),并且在退出函数体时会调用临时变量的析构函数。而在Release
模式下退出函数体不会调用返回值的构造函数,也不会调用临时变量的析构函数。