c++中返回值优化(RVO)和命名返回值优化(NRVO)介绍

RVO和NRVO介绍

前言

半年前就想写一篇关于RVONRVO的介绍,但碍于没什么时间去写博客。在跟身边人进行学术探讨的时候,会发现部分人可能尝到了编译器给它做返回值优化的好处,知道这段代码被优化了,但为什么 如何去做,却不知道。
因此,为了尝试说明RVONRVO的好处、使用场景、局限性等,在看了数个StackOverflow的讨论之后,有了这篇博客。

RVO(返回值优化(Return Value Optimization))

  • 如果函数返回一个无名的临时对象,该对象将被编译器移动或拷贝到目标中,在此时,编译器可以优化代码以减少对象的构造,即省略拷贝或移动。
T f() {
    return T();
}
f();        // 只有一次对T的默认构造函数的调用

NRVO(命名返回值优化(Named Return Value Optimization))

  • 命名返回值优化(指的是省略了命名对象的拷贝这一事实)
  • 如果函数按值返回一个类的类型,并且返回语句的表达式是一个具有自动存储期限的非易失性对象的名称(即不是一个函数参数),那么可以省略优化编译器所要进行的拷贝/移动。如果是这样的话,返回值就会直接构建在函数的返回值所要移动或拷贝的存储器(即被拷贝省略优化掉的拷贝或移动的存储器)中。

注意,NRVO不会应用于函数参数;它仅适用不是函数参数的局部变量,拿以下代码进行分析

#include <stdio.h>
class Data {
public:
    Data() { printf("I am in constructor\n"); }
    Data(const Data &data) { printf("I am in copy constructor\n"); }
    ~Data() { printf("I am in destructor\n"); }

    int mem_var;
};

Data process(int i) {
    Data data;              
    data.mem_var = i;
    return data;            
}

Data process1(Data data){
    return data;
}

int main() {
	printf("process\n");
    Data data;             
    data = process(5);      
	
	printf("process1\n");
    Data data2;
    data = process1(data2);
}

其结果(MSVC Debug模式下)是

process
I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
process1
I am in constructor
I am in copy constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
I am in destructor

其中最为核心的代码部分

Data process(int i) {
    Data data;              // 2 constructor
    data.mem_var = i;
    return data;            // 3 copy constructor
}

int main() {
    Data data;              // 1 constructor
    data = process(5);      // 5 destructor
}

process函数内部在编译器看来,可能是以下的行为过程

Data process(Data &_hiddenArg, int i){
    Data data;
    data.Data::Data();              // constructor for data
    data.mem_var = i;
    _hiddenArg.Data::Data(data);    // copy constructor for data
    return;
    data.Data::~Data();             // destructor for data
}

{
    Data data;                      // constructor for data
    data = process(5);
    // destructor for temp val
    // destructor for data
}

在经过NRVO优化后,代码如下,基本思想是消除临时基于堆栈的值并使用_hiddenArg(隐藏参数)。因此,这将消除基于堆栈的值的拷贝构造函数和析构函数

Data process(Data &_hiddenArg, int i){
    _hiddenArg.Data::Data();
    _hiddenArg.mem_var = i;
    Return 
}

其结果(MSVC Release模式)是

process
I am in constructor
I am in constructor
I am in destructor
process1
I am in constructor
I am in copy constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
I am in destructor

没有启用NRVO,在process函数中,构造了三次,析构了三次,有NRVO之后,拷贝构造函数操作就被优化省略掉了,可以看到优化是非常明显

例子来自微软:Named Return Value Optimization in Visual C++ 2005

关于NRVO的限制

函数return不同的命名对象

Data MyData(int i)
{
    Data data;
    data.mem_var = i;
    if (data.mem_var == 10)
        return (Data());
    return rvo;
}

其开不开NRVO结果都是

I am in constructor
I am in constructor
I am in copy constructor

当把所有返回路径改为data时,优化将消除拷贝构造函数

Data MyData(int i)
{
    Data data;
    data.mem_var = i;
    if (data.mem_var == 10)
        return (rvo);
    return rvo;
}

其结果是

I am in constructor
I am in constructor

Tips

若Data里有析构函数,则无法优化。拥有多个返回路径并引入析构函数会在函数中创建EH(Exception Handling)状态

NRVO的副作用

因为开NRVO的优化在某些情况下可以消除拷贝构造函数,但如果你的代码依赖于拷贝构造函数的副作用,那么你的代码就写的很烂。你编写的拷贝构造函数应该保证这样的优化时安全的(源自StackOverflow)。

最后的最后再附上一个cppreferenceexample

#include <iostream>
 
struct Noisy {
    Noisy() { std::cout << "constructed at " << this << '\n'; }
    Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
    Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
    ~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
 
Noisy f() {
    Noisy v = Noisy(); // copy elision when initializing v
    // from a temporary (until C++17)
    // from a prvalue (since C++17)
    return v; // NRVO from v to the result object (not guaranteed, even in C++17)
}             // if optimization is disabled, the move constructor is called
 
void g(Noisy arg) {
    std::cout << "&arg = " << &arg << '\n';
}
 
int main() {
    Noisy v = f(); // copy elision in initialization of v
    // from the temporary returned by f() (until C++17)
    // from the prvalue f() (since C++17)
    std::cout << "&v = " << &v << '\n';
 
    g(f());        // copy elision in initialization of the parameter of g()
    // from the temporary returned by f() (until C++17)
    // from the prvalue f() (since C++17)
}

其关闭NRVO可能的结果是

constructed at 008FF793
move-constructed
destructed at 008FF793
&v = 008FF7C7
constructed at 008FF78F
move-constructed
destructed at 008FF78F
&arg = 008FF7AC
destructed at 008FF7AC
destructed at 008FF7C7

开了之后是(c++11后,会尝试把局部变量通过Move-constructor出去(如未显式定义(将Noisy(Noisy&&那行代码注释)),则此处是Copy-constructor),NRVO则消除了在此过程中移动构造函数和析构函数的调用)

constructed at 00DEFA9E
&v = 00DEFA9E
constructed at 00DEFA9F
&arg = 00DEFA9F
destructed at 00DEFA9F
destructed at 00DEFA9E

RVO和NRVO的一些讨论思考

c++17之前,编译器允许使用RVONRVO进行优化,但这并不保证。从c++17开始,copy elision得到保证,强制使用RVO,但并不强制使用NRVO
gcc下提供标志-fno-elide-constructors,意味着你可以通过使用编译器标志来关闭NRVO

// 可关闭NRVO,RVO无法关闭 强制存在
g++ -std=c++17 -fno-elide-constructors main.cpp 

RVO总是可以应用的,不能普遍应用的是NRVO。为了进行优化,编译器必须知道在构造对象的地方将返回什么对象。

  • RVO的情况下(返回一个临时值),该条件很容易满足:对象是在return语句中构造的。(从上文RVONRVO篇幅短很多便可知)
  • NRVO的情况下,必须分析代码以了解编译器是否可以知道该信息。例如在函数返回对象前,抛出异常,可能会使得NRVO没有效果

RVO和NRVO的区别

  • RVO在函数返回临时对象时生效,NRVO适用于函数返回命名对象时生效
// RVO
Data DefaultData(){
    return Data();          // return temporary
}

Data data = DefaultData();
// NRVO
Data DefaultData(){
    Data data;
    data.mem_val = 10;
    return data;            // return named object
}

Data data = DefaultData();

wiki:wiki的copy elision介绍
cppreference高赞讨论:copy elision

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值