深入理解 Return Value Optimization
深入理解 Return Value Optimization
2019/01/30 09:35:11
讓我們用一個例子來看看 g++ 的 RVO ( Return Value Optimization ) 是怎麼工作的。
#include
using
namespace
std
;
int
c
=
0
;
class
Foo
{
public
:
explicit
Foo
();
Foo
(
const
Foo
&
other
);
~
Foo
();
int
value
;
};
ostream
&
operator
<<
(
ostream
&
out
,
const
Foo
&
v
)
{
out
<<
“Foo[”
<<
v
。
value
<<
“@”
<<
(
void
*
)
&
v
<<
“]”
;
return
out
;
}
Foo
::
Foo
()
:
value
(
c
++
)
{
cout
<<
“construct: ”
<<
*
this
<<
endl
;
}
Foo
::
Foo
(
const
Foo
&
other
)
:
value
(
other
。
value
)
{
cout
<<
“copy from ”
<<
other
<<
“to ”
<<
*
this
<<
endl
;
}
Foo
::~
Foo
()
{
cout
<<
“deconstructor: ”
<<
*
this
<<
endl
;
}
Foo
build
()
{
int
mark
=
0
;
cout
<<
“&mark ”
<<
(
void
*
)
&
mark
<<
endl
//
;
return
Foo
();
}
int
main
(
int
argc
,
char
*
argv
[])
{
cout
<<
“begin block”
<<
endl
;
{
int
begin
=
0
;
auto
obj
=
build
();
int
end
=
0
;
cout
<<
“&begin ”
<<
(
void
*
)
&
begin
<<
endl
//
<<
“obj = ”
<<
obj
<<
endl
//
<<
“&end ”
<<
(
void
*
)
&
end
<<
endl
//
;
}
cout
<<
“end block”
<<
endl
;
return
0
;
}
如果我們沒有指定編譯選項,g++ 預設打開了 RVO 的最佳化開關
+ g++ -std=c++11 test_rvo。cpp
+ 。/a。out
begin block
&mark 0x7fff5476766c
construct: Foo[0@0x7fff54767708]
&begin 0x7fff5476770c
obj = Foo[0@0x7fff54767708]
&end 0x7fff54767704
deconstructor: Foo[0@0x7fff54767708]
end block
我們知道 x86 平臺下,堆疊是向下生長的,也就是說,堆疊上的地址分配如下。
# main 函式的堆疊空間
0x7fff5476770c: &begin
0x7fff54767708: &obj
0x7fff54767704: &end
。。。
# build 函式的堆疊空間
0x7fff5476766c: &mark
可以看到,
build()
函式在構造
Foo
物件的時候,實際上使用的是
main
函式中的堆疊地址空間。換句話說,在呼叫
build
之前,
Foo
物件的記憶體就已經提前分配好了,使用的是
main
函式的堆疊地址空間,而
Foo
物件的初始化是在呼叫
build
函式之後執行的。
同時我們注意到,複製建構函式沒有被呼叫。
我們試試關閉 RVO 。
+ g++ -fno-elide-constructors -std=c++11 test_rvo。cpp
+ 。/a。out
begin block
&mark 0x7fff52a1366c
construct: Foo[0@0x7fff52a13668]
copy from Foo[0@0x7fff52a13668]to Foo[0@0x7fff52a13700]
deconstructor: Foo[0@0x7fff52a13668]
copy from Foo[0@0x7fff52a13700]to Foo[0@0x7fff52a13708]
deconstructor: Foo[0@0x7fff52a13700]
&begin 0x7fff52a1370c
obj = Foo[0@0x7fff52a13708]
&end 0x7fff52a136f0
deconstructor: Foo[0@0x7fff52a13708]
end block
-fno-elide-constructors
表示關閉 RVO 最佳化開關。
堆疊分析
# main 函式的堆疊空間
0x7fff52a1370c: &begin
0x7fff52a13708: &obj
0x7fff52a13700: &複製的臨時物件 tmp-obj2
0x7fff52a136f0: &end
# build 函式的堆疊空間
。。。
0x7fff52a1366c: &mark
0x7fff52a13668: &構造臨時物件 tmp-obj1
我們看到,複製建構函式被呼叫了兩次。我們仔細看一下發生了什麼。
在 main 函式的堆疊空間上,預留記憶體 0x7fff52a13700 ,準備接收返回值
tmp-obj2
。
在 build 函式的堆疊空間上,申請記憶體 0x7fff52a13668 ,並且構造了臨時物件
tmp-obj1
呼叫複製建構函式,把
tmp-obj1
複製到
tmp-obj2
上。
build 函式在返回之前,呼叫解構函式,析構掉
tmp-obj1
。
在 main 函式中,呼叫複製建構函式,把
tmp-obj2
複製構造到
obj
變數上。
立刻析構掉臨時物件
tmp-obj2
。
繼續在執行 main 函式之後的程式碼,在程式碼塊結束的時候,呼叫解構函式,析構掉
obj
物件。
RVO 的最佳化實在是太有用了,以至於編譯器預設是開啟這個最佳化開關的。
我們可以經常使用類似下面的程式碼
這種程式碼可讀性好,而且我們不用擔心效率的問題, RVO 可以保證程式碼十分高效的執行。
在實際專案中,我會看到下面的程式碼
這種方式可讀性不好。 作者本來的目的是防止多次複製物件,然而, 這樣通常導致一次多餘的函式呼叫。 因為一般我們在
foo
函數里面要構造一個物件,然後複製到
obj
。更糟糕的是,
obj
物件被初始化兩次,第一次初始化是在呼叫
foo
之前,使用預設建構函式。這個時候
obj
物件是一個無意義的物件。因為 RAII 的語義,導致這個設計是很醜陋的。