C++ 返回值优化RVO

返回值优化(Return Value Optimization, 简称RVO)是通过对源代码进行转换、消除对象的创建来实现加速程序,提升程序性能的优化措施,通常是由编译器实现的优化。

按值返回

假设我们打算实现Complex类,用于实现复数。可以这样设计Complex类:

class Complex
{
    // Complex addition operator
    friend Complex operator+(const Complex&, const Complex&);

public:
    // Default ctor
    Complex(double r = 0.0, double i = 0.0)
    : real(r), imag(i)
    {}

    // Copy ctor
    Complex(const Complex& c)
    : real(c.real), imag(c.imag)
    {}

    // Assignment operator
    Complex& operator=(const Complex& c);

    ~Complex(){}
private:
    double real;
    double imag;
};

我们是如何实现友元函数operator+(const Complex&, const Complex&),以实现两个复数相加的?
通常,我们这样写:

// 注意operator+是重载函数, 不是Complex类成员函数
Complex operator+(const Complex& a, const Complex& b)
{
    Complex retVal;
    retVal.real = a.real + b.real;
    retVal.imag = a.imag + b.imag;
    return retVal;
}

应用时,将Comlex对象c1, c2, c3按如下方式进行操作:

c3 = c1 + c2;

如何将c1+c2的值放到c3呢?
如果没有任何优化,调用运算符operator+时调用Complex构造函数,构造retVal对象;返回给c3时,又调用Complex的拷贝构造函数。也就是说,会构造两次Complex对象。

返回值优化

针对上面这种情形,编译器有专门的优化方式:编译器创建一个临时__result对象,作为一个专门的参数,按引用传递方式将结果传递给Complex::operator+()。
也就是说,编译器会把重载函数operator+重写为另一个稍有不同的函数:

void Complex_Add(const Complex& __result, const Complex& c1, const Complex& c2)
{
    ...
}

此时,原来的源代码c3 = c1 + c2,会被转换成(伪代码):

struct Complex __tempResult;         // 存储, 没有构造
Complex_Add(__tempResult, c1, c2);   // 所有参数引用传递
c3 = __tempResult;                   // 反馈结果给c3

这就是所谓返回值优化,编译器通过消除局部对象retVal并将其替换为__tempResult,优化Complex_Add()。

  • 编译器不执行RVO通常情形

然而,并不是所有编译器会对这种返回值是临时对象的函数,执行RVO。通常,下面两种情况下,编译器不执行RVO:
1)复杂函数;
2)函数有多个return语句,并且返回不同名称的对象。

  • 有一种编译器拒绝对以下特定版本operator+执行RVO
// operator+版本1
Complex operator+(const Complex& a, const Complex& b)
{
    Complex retVal;
    retVal.real = a.real + b.real;
    retVal.imag = a.imag + b.imag;
    return retVal;
}

// operator+版本2
Complex operator+(const Complex& a, const Complex& b)
{
    Complex retVal(a.real + b.real, a.imag + b.imag);
    return retVal;
}
  • 编译器会对以下版本使用RVO
// operator+版本3
Complex operator+(const Complex& a, const Complex& b)
{
    double r = a.real + b.real;
    double i = a.imag + b.imag;
    return Complex(r, i);
}

// operator+版本4
Complex operator+(const Complex& a, const Complex& b)
{
    return Complex(a.real + b.real, a.imag + b.imag);
}

区别在于版本1、2为临时对象使用了命名变量,版本3、4使用匿名变量。

如果想让编译器为命名变量执行RVO,可以在返回时使用std::move,将左值转换为右值。

计算性构造函数

编译器无法执行RVO时,可以用计算性构造函数。比如上面Complex例子中,我们可以实现版本5的operator+,在其中创建一个默认的匿名Complex对象并推迟成员变量的设置,随后对对象成员进行初始化。这个构造函数,就是计算性构造函数。

// operator+版本5
Complex operator+(const Complex& a, const Complex& b)
{
    return Complex(a, b);
}

// 计算性构造函数
Complex::Complex(const Complex& x, const Complex& y)
: real(x.real + y.real), imag(x.imag + y.imag)
{}

计算性构造函数缺点:必须为每种运算(如加、减、乘、除)符号,添加不同的计算性构造函数。

关闭RVO

可以通过编译选项-fno-elide-constructors,关闭RVO功能。

参考

[1]DovBulka, DavidMayhew, 布尔卡,等. 提高C++性能的编程技术[M]. 清华大学出版社, 2003.
[2]C++返回值拷贝以及std::move | CSDN

posted @ 2023-01-08 23:39  明明1109  阅读(633)  评论(0编辑  收藏  举报