C++的RVO优化


RVO优化

本问对C++中RVO和NRVO进行讲解。

1、NRVO和RVO

​ RVO(Return Value optimization,返回值优化)和NRVO(Named Return Value Optimization,命名返回值优化)是C++编译器对局部对象的一种优化,用于消除函数返回值时产生的临时对象和拷贝的额外开销,而是在目标位置构造返回值的优化技术,用于避免不必要的临时开销,RVO在C++98时作为性能提升常见的优化手段,但那时候并未强制,首次明确允许是在C++11中,而C++17变为强制要求。

2、优化对比

C++
#include 
using namespace std;

class Test
{
public:
    Test() { cout << "构造" << endl; }
    Test(const Test &T)
    {
        cout << "拷贝构造" << endl;
        // ...
    }
    ~Test() { cout << "析构" << endl; }
};

Test getTest()
{
    Test T;
    return T;
}

int main()
{
    Test t = getTest();
    return 0;
}

2.1 未开启优化

关闭优化:

shell
g++ -std=c++11 main.cpp -o main.exe  -fno-elide-constructors

​ 如上代码若没有开RVO优化,会输出如下内容:

text
构造          // getTest() 中的局部对象 T 构造
拷贝构造       // return T → 拷贝到临时对象
析构          // getTest() 中的 T 析构(函数退出)
拷贝构造       // 临时对象拷贝到 main 的 t
析构          // 临时对象析构(表达式结束)
析构          // main 的 t 析构(程序退出)

​ 在这个执行过程中,出现了很多不必要的开销,而RVO就是用来解决该问题的。

需要注意的时,当不强行指定C++11时,输出结果可能是如下这样的:

text
构造
拷贝构造
析构
析构

这是由于移动语义的干扰。

2.2 开启优化

​ 开启RVO优化后,输出的内容如下:

C++
构造
析构

​ 高下立判,在编译器优化的时候,它会跳过不必要的拷贝,直接构造和析构。

3、优化原理

​ 如上代码中,RVO优化直接在Test t = getTest()中预留一个内存位置,来避免重复拷贝。也就是说,RVO的优化是局部对象的创建并非在函数的栈帧中,而是在main函数t的位置,这个位置预留了一个空间,等待getTest函数中的T被实例化(注意:预留空间并非体现初始化,不会影响代码执行顺序),实例化的位置是之前被预留的位置,从而避免不必要的拷贝。

4、RVO和NRVO的区别

​ RVO和NRVO都是C++编译器为了消除不必要的对象拷贝而采用的优化技术。它们的核心区别在于 优化的触发条件应用场景

4.1 NRVO

​ 本质是返回一个函数内部的实例对象,例如现在函数中构造一个对象,再返回它。一般用来优化局部对象存在名字的情况,需要分析生命周期和代码路径,优化较为复杂,并且在多路径返回的情况下容易失效。

4.2 RVO

​ 直接返回一个对象(对象无名字),在局部中并没有初始化的情况,这种情况下编译器更容易分析,优化确定性很高,在C++17中,它甚至是强制性的。

4.3 NRVO可能失效的地方

​ 即使到C++23,NRVO仍是可选的优化。开发者无法完全依赖它,例如下:

C++
#include 
using namespace std;

class Test
{
public:
    Test(int a) { cout << "构造" << endl; }
    Test() { cout << "构造" << endl; }
    Test(const Test &T)
    {
        cout << "拷贝构造" << endl;
        // ...
    }
    ~Test() { cout << "析构" << endl; }
};

Test getTest()
{
    Test T;
    return T;
}

Test createNRVO(bool flag)
{
    Test a(1), b(2);
    if (flag)
    {
        return a; // 尝试NRVO优化a到调用方内存
    }
    else
    {
        return b; // 若返回b,编译器可能无法确定目标地址
    }
}

int main()
{
    Test t = createNRVO(true);
    return 0;
}

以上内容输出的结果是:

text
构造
构造
拷贝构造
析构
析构
析构

4.4 如何细分

  1. 优先使用RVO
    在简单场景中,直接返回临时对象(如return Test(args);),充分利用C++17的强制优化。
  2. 为NRVO创造条件
    • 保持函数单一返回路径(如只在末尾返回一个具名对象)。
    • 避免在返回前对对象进行复杂操作(如多分支修改)。
  3. 结合移动语义:
    • 为类实现移动构造函数作为备选方案。

总结:本质上无论是NRVO或RVO说的都是一个东西,做的是同一件事,它们内部实现的方式和触发条件不一致而已。但是NRVO更加宽松,它是否优化并未是一个标准的东西,开发者无法完全的依赖它,而RVO是确定性的优化,在使用时,可以根据4.1和4.2标题中的的解释来酌情使用,并且没有特殊条件时,优先使用RVO。


文章作者: 埃芒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 埃芒 !