共計 4185 個字符,預計需要花費 11 分鐘才能閱讀完成。
這篇文章主要講解了“怎么理解 C ++ 中的 RVO”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著丸趣 TV 小編的思路慢慢深入,一起來研究和學習“怎么理解 C ++ 中的 RVO”吧!
前言
考慮存在這樣一個類如 HeavyObject,其拷貝賦值操作比較耗時,通常你在使用函數返回這個類的一個對象時會習慣使用哪一種方式?或者會根據具體場景選擇某一種方式?
// style 1
HeavyObject func(Args param);
// style 2
bool func(HeavyObject* ptr, Args param);
上面的兩種方式都能過到同樣的目的,但直觀上的使用體驗的差別也是非常明顯的:
style 1 只需要一行代碼,而 style 2 需要兩行代碼
// style 1
HeavyObject obj = func(params);
// style 2
HeavyObject obj;
func(obj, params);
但是,能達到同樣的目的,消耗的成本卻未必是一樣的,這取決于多個因素,比如編譯器支持的特性、C++ 語言標準的規范強制性、多團隊多環境開發等等。
看起來 style 2 雖然使用時需要寫兩行代碼,但函數內部的成本卻是確定的,只會取決于你當前的編譯器,外部即使采用不同的編譯器進行函數調用,也并不會有多余的時間開銷和穩定性問題。比如 func 內部使用 clang+libc++ 編譯,外部調用的編譯環境為 gcc+gnustl 或者 vc++,除了函數調用開銷,不用擔心其它性能開銷以及由于編譯環境不同會崩潰問題。
因此這里我主要剖析一下 style 1 背后開發者需要關注的點。
RVO
RVO 是 Return Value Optimization 的縮寫,即返回值優化,NRVO 就是具名的返回值優化,為 RVO 的一個變種,此特性從 C ++11 開始支持,也就是說 C ++98、C++03 都是沒有將此優化特性寫到標準中的,不過少量編譯器在開發過程中也會支持 RVO 優化(如 IBM Compiler?),比如微軟是從 Visual Studio 2010 才開始支持的。
仍然以上述的 HeavyObject 類為例,為了更清晰的了解編譯器的行為,這里實現了構造 / 析構及拷貝構造、賦值操作、右值構造函數,如下
class HeavyObject
public:
HeavyObject() { cout Constructor\n }
~HeavyObject() { cout Destructor\n }
HeavyObject(HeavyObject const) { cout Copy Constructor\n }
HeavyObject operator=(HeavyObject const) { cout Assignment Operator\n return *this; }
HeavyObject(HeavyObject) { cout Move Constructor\n }
private:
// many members omitted...
};
編譯環境:
AppleClang 10.0.1.10010046
* 第一種使用方式
HeavyObject func()
return HeavyObject();
// call
HeavyObject o = func();
按照以往對 C ++ 的理解,HeavyObject 類的構造析構順序應該為
Constructor
Copy Constructor
Destructor
Destructor
但是實際運行后的輸出結果卻為
Constructor
Destructor
實際運行中少了一次拷貝構造和析構的開銷,編譯器幫助我們作了優化。
于是我反匯編了一下:
0000000100000f60 __Z4funcv :
100000f60: 55 push %rbp
100000f61: 48 89 e5 mov %rsp,%rbp
100000f64: 48 83 ec 10 sub $0x10,%rsp
100000f68: 48 89 f8 mov %rdi,%rax
100000f6b: 48 89 45 f8 mov %rax,-0x8(%rbp)
100000f6f: e8 0c 00 00 00 callq 100000f80 __ZN11HeavyObjectC1Ev
100000f74: 48 8b 45 f8 mov -0x8(%rbp),%rax
100000f78: 48 83 c4 10 add $0x10,%rsp
100000f7c: 5d pop %rbp
100000f7d: c3 retq
100000f7e: 66 90 xchg %ax,%ax
上述匯編代碼中的__Z4funcv 即 func() 函數,__ZN11HeavyObjectC1Ev 即 HeavyObject::HeavyObject()。
不同編譯器的 C ++ 修飾規則略有不同。
實際上這里就是先創建外部的對象,再將外部對象的地址作為參數傳給函數 func,類似 style 2 方式。
* 第二種使用方式
HeavyObject func()
HeavyObject o;
return o;
// call
HeavyObject o = func();
運行上述調用代碼的結果為
Constructor
Destructor
與第一種使用方式的結果相同,這里編譯器實際做了 NRVO,來看一下反匯編
0000000100000f40 __Z4funcv : // func()
100000f40: 55 push %rbp
100000f41: 48 89 e5 mov %rsp,%rbp
100000f44: 48 83 ec 20 sub $0x20,%rsp
100000f48: 48 89 f8 mov %rdi,%rax
100000f4b: c6 45 ff 00 movb $0x0,-0x1(%rbp)
100000f4f: 48 89 7d f0 mov %rdi,-0x10(%rbp)
100000f53: 48 89 45 e8 mov %rax,-0x18(%rbp)
100000f57: e8 24 00 00 00 callq 100000f80 __ZN11HeavyObjectC1Ev // HeavyObject::HeavyObject()
100000f5c: c6 45 ff 01 movb $0x1,-0x1(%rbp)
100000f60: f6 45 ff 01 testb $0x1,-0x1(%rbp)
100000f64: 0f 85 09 00 00 00 jne 100000f73 __Z4funcv+0x33
100000f6a: 48 8b 7d f0 mov -0x10(%rbp),%rdi
100000f6e: e8 2d 00 00 00 callq 100000fa0 __ZN11HeavyObjectD1Ev // HeavyObject::~HeavyObject()
100000f73: 48 8b 45 e8 mov -0x18(%rbp),%rax
100000f77: 48 83 c4 20 add $0x20,%rsp
100000f7b: 5d pop %rbp
100000f7c: c3 retq
100000f7d: 0f 1f 00 nopl (%rax)
從上面的匯編代碼可以看到返回一個具名的本地對象時,編譯器優化操作如第一種使用方式一樣直接在外部對象的指針上執行構造函數,只是如果構造失敗時還會再調用析構函數。
以上兩種使用方式編譯器所做的優化非常相近,兩種方式的共同點都是返回本地的一個對象,那么當本地存在多個對象且需要根據條件選擇返回某個對象時結果會是如何呢?
* 第三種使用方式
HeavyObject dummy(int index)
HeavyObject o[2];
return o[index];
// call
HeavyObject o = dummy(1);
運行后的結果為
Constructor
Constructor
Copy Constructor
Destructor
Destructor
Destructor
從運行的結果可以看到沒有做 RVO 優化,此時調用了拷貝構造函數。
從上述三種實現方式可以看到,如果你的函數實現功能比較單一,比如只會對一個對象進行操作并返回時,編譯器會進行 RVO 優化;如果函數實現比較復雜,可能會涉及操作多個對象并不確定返回哪個對象時,編譯器將不做 RVO 優化,此時函數返回時會調用類的拷貝構造函數。
但是,當只存在一個本地對象時,編譯器一定會做 RVO 優化嗎?
* 第四種使用方式
HeavyObject func()
return std::move(HeavyObject());
// call
HeavyObject o = func();
實際運行輸出的結果是
Constructor
Move Constructor
Destructor
Destructor
上述的函數實現直接返回臨時對象的右值引用,從實際的運行結果來看調用了 Move 構造函數,與第一種使用方式運行的結果明顯不同,并不是我期望的只調用一次構造函數和析構函數,也就是說編譯器沒有做 RVO。
* 第五種使用方式
HeavyObject func()
HeavyObject o;
return static_cast HeavyObject
// call
HeavyObject o = func();
實際運行輸出的結果是
Constructor
Copy Constructor
Destructor
Destructor
上述的函數實現直接返回本地對象的引用,實際運行結果仍然調用了拷貝構造函數,并不是期望的只調用一次構造和析構函數,也就是說編譯器并沒有做 RVO。
感謝各位的閱讀,以上就是“怎么理解 C ++ 中的 RVO”的內容了,經過本文的學習后,相信大家對怎么理解 C ++ 中的 RVO 這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是丸趣 TV,丸趣 TV 小編將為大家推送更多相關知識點的文章,歡迎關注!