编译器拷贝优化—— RVO 与 NRVO Shepard-Wang

编译器拷贝优化是一种可以消除不必要的拷贝或转移的编译器优化技术。主要通过 RVO 和 NRVO 两种方式来实现。

g++ 支持 RVO,NRVO这三种优化,可以通过编译选项:-fno-elide-constructors来关闭优化

NRVO

命名返回值优化 name return value optimization :如果一个函数按值返回类类型且返回语句表达式是自动存储周期(临时变量,且不是函数参数)的非 volatile 对象,那么拷贝或转移(copy or move)会被编译器省去。如果是这样,返回值会直接在函数的返回值本应该被拷贝或转移的内存上创建。

为了防止我翻译的不清楚,以下是原文:

NRVO (Named Return Value Optimization): If a function returns a class type by value and the return statement’s expression is the name of a non-volatile object with automatic storage duration (which isn’t a function parameter), then the copy/move that would be performed by a non-optimising compiler can be omitted. If so, the returned value is constructed directly in the storage to which the function’s return value would otherwise be moved or copied.

A MyMethod (B &var)
{
    A retVal;
    retVal.member = var.value + bar(var);
    return retVal;
}

使用上述函数的程序可能具有如下构造:

valA = MyMethod(valB);

从MyMethod返回的值是在valA通过使用隐藏的参数所指向的内存空间中创建的。下面是当我们公开隐藏的参数并显示地显示构造函数和析构函数时的功能:

A MyMethod (A &_hiddenArg, B &var)
{
   A retVal;
   retVal.A::A(); // constructor for retVal
   retVal.member = var.value + bar(var);
   _hiddenArg.A::A(retVal);  // the copy constructor for A
   return;
retVal.A::~A();  // destructor for retVal
}

上段代码为不使用 NRVO 的隐藏参数代码(伪代码) 从上面的代码可以看出,有一些优化的机会。其基本思想是消除基于堆栈的临时值( retVal )并使用隐藏的参数。因此,这将消除基于堆栈的值的拷贝构造函数和析构函数。下面是基于 NRVO 的优化代码:

A MyMethod(A &_hiddenArg, B &var)
{
    _hiddenArg.A::A();
    _hiddenArg.member = var.value + bar(var);
    return;
}

C++ 命名返回值优化(NRVO) 里面有几个 NRVO 的示例,讲解的不错,这里不复制过来了。

总结:

区别就在于,当 NRVO 关闭时,类类型的返回值会先被创建好,然后隐藏参数以返回值为参数拷贝构造;而当 NRVO 开启时,隐藏参数直接被构造,取代原本的返回值。

NRVO 不被启动的情况

  1. 在遇到异常时,隐藏的参数必须在它正在替换的临时范围内被破坏。

    // RVO class is defined above in figure 4
    #include <stdio.h>
       
    class RVO
    {
    public:
            RVO() { printf("I am in constructor\n"); }
            RVO( const RVO& c_RVO ) { printf("I am in copy constructor\n"); }
            ~RVO() { printf("I am in destructor\n"); }
            int mem_var;
    };
       
    RVO MyMethod(int i)
    {
        RVO rvo;
        rvo.mem_var = i;
        //throw "I am throwing an exception!";
        return rvo;
    }
       
    int main()
    {
        RVO rvo;
        try
        {
            rvo = MyMethod(5);
        }
        catch(const char* str)
        {
            printf("I caught the exception\n");
        }
    }
    
  2. 若要使用优化,所有退出路径必须返回同一命名对象。

    #include <stdio.h>
    class RVO
    {
    public:
            RVO()
            {
                    printf("I am in construct\n");
            }
       
            RVO(const RVO& c_RVO)
            {
                    printf("I am in copy construct\n");
            }
       
            int mem_var;
    };
       
    RVO MyMethod(int i)
    {
            RVO rvo;
            rvo.mem_var = i;
            if( rvo.mem_var == 10 )
                    return RVO();
            return rvo;
    }
       
    int main()
    {
            RVO rvo;
            rvo = MyMethod(5);
    }
       
    

注意优化的副作用

程序员应该意识到这种优化可能会影响应用程序的流程。下面的示例说明了这种副作用:

#include <stdio.h>

int NumConsCalls = 0;
int NumCpyConsCalls = 0;

class RVO
{
public:
        RVO()
        {
                NumConsCalls ++;
        }

        RVO(const RVO& c_RVO)
        {
                NumCpyConsCalls++;
        }
};

RVO MyMethod()
{
        RVO rvo;
        return rvo;
}

int main()
{
        RVO rvo;
        rvo = MyMethod();
        int Division = NumConsCalls / NumCpyConsCalls;
        printf("Construct calls / Copy constructor calls = %d\n", Division);
}
编译未启用优化将产生大多数用户所期望的。“构造函数”被调用两次。“拷贝构造函数”被调用一次。因此除法生成2。
Constructor calls / Copy constructor calls = 2

另一方面,如果上面的代码通过启用优化进行编译,NRVO 将会启用。因此“拷贝构造函数”调用将被删除。因此,NumCpyConsCalls 将为零,导致异常。如果没有适当处理,可以导致应用程序崩溃。

RVO

返回值优化 return value optimization:如果函数返回的是一个匿名临时对象,编译器会取消拷贝或转移

原文:

RVO (Return Value Optimization): If the function returns a nameless temporary object that would be moved or copied into the destination by a naive compiler, the copy or move can be omitted as per 1.

#include<iostream>

using namespace std;

class RVO
{
  public:
    RVO()
    {cout << "constructor" << endl;}
    RVO(int i)
    {
      _a = i;
      cout << "constructor" << endl;
    }
    RVO(const RVO& rvo)
    {cout << "copy constructor" << endl;}
    ~RVO()
    {cout << "delete" << endl;}

    int _a;
};

RVO func(int i)
{
  return RVO(i);
}


int main()
{
  RVO rvo(func(5)) ;

  return 0;
}

如果不开启 NRVO,运行结果如下:

constructor -> func 函数构造 RVO 匿名对象 
copy constructor -> func 函数构造隐式参数
delete -> 删除 RVO 匿名对象
copy constructor -> 构造 rvo 对象
delete
delete

开启 NRVO,运行结果:

constructor -> 直接构造 rvo 对象
delete

NRVO,RVO 与 move

#include <iostream>  
using namespace std;

class ABC  
{  
public:   
    const char *a;  
    ABC()  
     { cout<<"Constructor"<<endl; }  
    ABC(const char *ptr)  
     { cout<<"Constructor"<<endl; }  
    ABC(ABC  &obj)  
     { cout<<"copy constructor"<<endl;}  
    ABC(ABC&& obj)  
    { cout<<"Move constructor"<<endl; }  
    ~ABC()  
    { cout<<"Destructor"<<endl; }  
};

ABC fun123()  
{ ABC obj; return obj; }  

ABC xyz123()  
{  return ABC(); }  

int main()  
{  
    ABC abc;  
    ABC obj1(fun123());    //NRVO  
    ABC obj2(xyz123());    //RVO, not NRVO 
    ABC xyz = "Stack Overflow";//RVO  
    return 0;  
}

**Output without -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Destructor  
Destructor  
Destructor  
Destructor  

编译拷贝优化的好处

每一次编译器拷贝优化,一次构造和析构被取消,这节省了 CPU 时间;一个对象没有被创建,这节省了栈帧空间。

参考: