RVO (return value optimization) 和NRVO (named return value optimization) 是C++在處理 “返回一個class object的函數” 時常用的優化技術,主要作用就是消除臨時對象的構造和析構成本。目前我正在研究《深度探索C++對象模型》,對於這兩種常見的編譯器優化有了初步的瞭解。接下來以一個名叫Point3d的類和一個factory函數爲例來總結一下。
Point3d類定義如下:
- class Point3d
- {
- public:
- Point3d(int x = 0, int y = 0, int z = 0): x(x), y(y), z(z)
- {
- cout << "constructor ";
- cout << this->x << " " << this->y << " " << this->z << endl;
- }
- Point3d(const Point3d &other): x(other.x), y(other.y), z(other.z)
- {
- cout << "copy constructor" << endl;
- }
- ~Point3d()
- {
- cout << "destructor" << endl;
- }
- Point3d& operator=(const Point3d &rhs)
- {
- if (this != &rhs)
- {
- this->x = rhs.x;
- this->y = rhs.y;
- this->z = rhs.z;
- }
- cout << "operator = " << endl;
- return *this;
- }
- int x;
- int y;
- int z;
- };
我們定義了三個成員變量x, y, z,爲了簡單起見,我們將它們都定義爲public訪問屬性。接下來,我們定義了該類的默認構造函數、拷貝構造函數、賦值運算符和析構函數,它們都會通過打印一些字符來追蹤函數調用。爲了簡單起見,我們不定義移動構造函數和移動賦值運算符。
接下來我們定義一個函數factory,它產生並返回一個Point3d對象:
- Point3d factory()
- {
- Point3d po(1, 2, 3);
- return po;
- }
整體代碼如下:
- #include <iostream>
- using namespace std;
- class Point3d
- {
- public:
- Point3d(int x = 0, int y = 0, int z = 0): x(x), y(y), z(z)
- {
- cout << "constructor ";
- cout << this->x << " " << this->y << " " << this->z << endl;
- }
- Point3d(const Point3d &other): x(other.x), y(other.y), z(other.z)
- {
- cout << "copy constructor" << endl;
- }
- ~Point3d()
- {
- cout << "destructor" << endl;
- }
- Point3d& operator=(const Point3d &rhs)
- {
- if (this != &rhs)
- {
- this->x = rhs.x;
- this->y = rhs.y;
- this->z = rhs.z;
- }
- cout << "operator = " << endl;
- return *this;
- }
- int x;
- int y;
- int z;
- };
- Point3d factory();
- int main()
- {
- Point3d p = factory();
- cout << p.x << " " << p.y << " " << p.z << endl;
- return 0;
- }
- Point3d factory()
- {
- Point3d po(1, 2, 3);
- return po;
- }
接下來我們討論“不做任何返回值優化”、“只做RVO不做NRVO”和“不光做RVO也做NRVO”三種情況
(0)不做任何返回值優化
gcc中有一個-fno-elide-constructors的命令,可以去掉任何返回值優化。我們在編譯時加上這個命令,觀察到程序的輸出如下。我們加上註釋來說明程序運行過程
constructor 1 2 3 //構造出factory()中的局部對象po
copy constructor //調用Point3d的拷貝構造函數,用po構造出一個臨時對象,姑且稱之爲臨時對象_temp
destructor //析構factory()中的局部對象po
copy constructor //調用Point3d的拷貝構造函數,用臨時對象_temp構造出main()函數中的局部對象p
destructor //析構臨時對象_temp
1 2 3 //輸出x, y, z
destructor //析構main()函數中的局部對象p
可以看到,不做任何返回值優化時我們會承擔兩次拷貝構造函數和兩次析構函數調用的成本。
(1)使用RVO而不用NRVO
VS在debug模式下使用RVO而不用NRVO。我們可以將原來的代碼在VS中進行測試,觀察到程序的輸出如下:
constructor 1 2 3 //構造出factory()中的局部對象pocopy constructor
destructor
1 2 3 //輸出x, y, z
destructor //析構main()函數中的局部對象p
我們可以發現,使用了RVO之後我們減少了一次拷貝構造函數和一次析構函數的調用。
事實上,RVO的原理是,將“返回一個類對象的函數”的返回值當做該函數的參數處理。具體而言,在上面的例子中,factory()函數會被改寫成如下的形式:
- //c++僞代碼
- void factory(Point3d &_result)
- {
- Point3d po; //不做初始化
- po.Point3d::Point3d(1, 2, 3); //調用構造函數構造po
- _result.Point3d::Point3d(po); //調用拷貝構造函數構造factory()函數參數
- po.Point3d::~Point3d(); //析構po
- return; //沒有返回值
- }
而對應的函數調用則會被改寫成如下的形式:
- Point3d p; //不做初始化
- factory(p); //將p變爲函數參數,即調用函數factory()來初始化p
這樣我們就可以很清楚地發現,上文的輸出結果裏拷貝構造函數的調用是爲了從factory()中的局部對象po構造出函數參數_result,而析構函數的調用則是爲了析構po。換句話說,進行了RVO之後,我們的factory()函數只使用了一個叫做po的局部對象,接下來該函數利用這個po對象直接構造出了factory()函數外面,main()函數裏面的對象p。
這樣做當然比不使用任何返回值優化要好,因爲它減少了一次拷貝構造函數的調用和一次析構函數的調用。然而我們偉大的工程師們依然不知足。能不能把這個局部對象po也給省略掉呢?換句話說,能不能讓factory()函數直接構造出對象p呢?如果能的話,我們就可以再次減少一次拷貝構造函數(用來利用po構造出_result)和一次析構函數(用來析構po)的調用。
(2)在只使用RVO不使用NRVO時再次優化拷貝構造函數和析構函數的調用
我們可以將factory()函數改寫成如下的形式:
- Point3d factory()
- {
- return Point3d(1, 2, 3);
- }
運行程序,得到的輸出如下:
constructor 1 2 3
1 2 3
destructor
我們可以發現,和上面(1)中的輸出相比,減少了一次拷貝構造函數和析構函數的調用。爲什麼呢?
實際上,在這時,factory()函數被改寫成如下形式:
- //c++僞代碼
- void factory(Point3d &_result)
- {
- _result.Point3d::Point3d(1, 2, 3); //調用構造函數構造factory()函數參數
- return; //沒有返回值
- }
main()中的factory()函數調用依然保持(1)中的形式不變
- Point3d p; //不做初始化
- factory(p); //將p變爲函數參數,即調用函數factory()來初始化p
這時,由於我們的factory函數返回的是一個匿名的Point3d對象,編譯器就可以進行更進一步的優化,省略掉factory()中的局部對象的構造、析構和從這個局部對象到函數參數的拷貝,從而減少一次拷貝構造函數和一次析構函數的調用。這時,我們可以認爲,factory()函數直接構造出了main()函數中的p對象。
(3)既使用RVO又使用NRVO
(2)的不足之處在於程序員必須通過手動返回臨時對象來優化代碼。NRVO使得在程序員寫出和(1)中相同代碼的情況下也能起到(2)中的效果,即,讓factory()函數直接構造出main()函數中的p對象。
這時,factory()函數的寫法依然和(1)中而不是(2)中相同。
- Point3d factory()
- {
- Point3d po(1, 2, 3);
- return po;
- }
編譯器會用_result直接替換po,也就是改寫成如下代碼:
- //c++僞代碼
- void factory(Point3d &_result)
- {
- _result.Point3d::Point3d(1, 2, 3); //調用構造函數構造factory()函數參數
- return; //沒有返回值
- }
當然,如果程序員寫出的是這樣的代碼:
- Point3d factory()
- {
- Point3d po;
- po.x = 1;
- po.y = 2;
- po.z = 3;
- return po;
- }
則會被改寫爲:
- //c++僞代碼
- void factory(Point3d &_result)
- {
- _result.Point3d::Point3d(); //調用構造函數構造factory()函數參數
- _result.x = 1;
- _result.y = 2;
- _result.z = 3;
- return; //沒有返回值
- }
(如果只使用RVO則會被改寫成如下代碼)
- //c++僞代碼
- void factory(Point3d &_result)
- {
- Point3d po;
- po.Point3d::Point3d(); //調用構造函數構造factory()局部對象
- po.x = 1;
- po.y = 2;
- po.z = 3;
- _result.Point3d::Point3d(po); //調用拷貝構造函數構造factory()函數參數
- return; //沒有返回值
- }
可以很明顯地看出來:1、在RVO機制上加上NRVO機制的直接表現就是編譯器直接用“用來替代函數返回值的參數”取代“該函數返回的那個局部對象”,在這裏,表現爲編譯器直接使用_result取代了po對象。2、節約了一次拷貝構造函數和一次析構函數調用的成本。因爲“該函數返回的那個局部對象”被“用來代替函數返回值的那個參數”所取代,所以我們無需構造出該局部對象,自然也無需析構它。
目前的常用c++編譯器都支持NRVO,C++11也已經把“允許編譯器進行NRVO”寫入了標準。經過測試,gcc編譯器在debug和release模式下均支持NRVO,VS在debug模式下不支持NRVO,僅支持RVO,而在release模式下也支持NRVO。