深入理解 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 的語義,導致這個設計是很醜陋的。