條款1:Improve Performance of C++ Codes (1) -- 使用初始化列表還是指派陳述式?
繼續記錄:
條款2:
--------------------------------
Q:何時會出現臨時對象以及如何消除臨時對象?
A:為了使函數成功調用而進行隱式類型轉換以及函數返回對象時可能會出現臨時對象。對於前者,可以採取方法避免隱式類型轉換,對於後者,盡量配合編譯器的傳回值最佳化(RVO)來消除臨時對象。
--------------------------------
在<<More Effictive C++>>中,有關於臨時對象的這麼一段:在C++中真正的臨時對象是看不見的,它們不出現在你的原始碼中。建立一個沒有命名(unamed)的非堆(non-heap)對象會產生臨時對象。這種未命名的對象通常在兩種條件下產生:
a.為了使函數成功調用而進行隱式類型轉換。
b.函數返回對象時。
a.首先考慮為使函數成功調用而建立臨時對象這種情況。
測試:
--------------------------------
下面看個c++ test program:
//TestClass.h
class A
{
public:
A();
int i;
};
class B
{
public:
B();
B(const A& a);
A a;
int j;
};
//TestClass.cpp
#include "TestClass.h"
#include <iostream>
using namespace std;
A::A()
{
cout<<"A:A()"<<endl;
}
B::B()
{
cout<<"B:B()"<<endl;
}
B::B(const A& a)
{
cout<<"B::B(const A& a)"<<endl;
}
// main.cpp
#include "LGVector.h"
#include "LGVector.h"
void test(A a)
{
}
void test1(B b)
{
}
void test2(const B& b)
{
}
void test3(B& b)
{
}
int main(int argc, char* argv[])
{
A a;
test(a); // 將實參a壓棧,並不會調用建構函式產生臨時對象
test1(a);
test2(a);
//test3(a); // Compile error!!! 原因:C++禁止為非常量引用(reference-to-non-const)產生臨時對象
return 0;
}
下面是main()的Assembly codes:
(x86, VC++ 6,Release, Optimization: Maximize Speed)
_a$ = -12
$T300 = -8
_main PROC NEAR ; COMDAT
; 23 : {
sub esp, 12 ; 0000000cH
; 24 : A a;
lea ecx, DWORD PTR _a$[esp+12]
call ??0A@@QAE@XZ ; A::A
; 25 :
; 26 : test(a);
mov eax, DWORD PTR _a$[esp+12]
push eax
call ?test@@YAXVA@@@Z ; test
; 27 : test1(a); // 將實參a壓棧,並不會調用建構函式產生臨時對象
push ecx
lea edx, DWORD PTR _a$[esp+20]
mov ecx, esp
push edx // 將實參a壓棧
call ??0B@@QAE@ABVA@@@Z ; B::B // 拷貝構造被調用來產生臨時對象
call ?test1@@YAXVB@@@Z ; test1
add esp, 8
; 28 : test2(a);
lea eax, DWORD PTR _a$[esp+12]
lea ecx, DWORD PTR $T300[esp+12]
push eax
call ??0B@@QAE@ABVA@@@Z ; B::B // 拷貝構造被調用來產生臨時對象
lea ecx, DWORD PTR $T300[esp+12]
push ecx
call ?test2@@YAXABVB@@@Z ; test2
; 29 : //test3(a); // Compile error!!! 原因:C++禁止為非常量引用(reference-to-non-const)產生臨時對象
; 30 :
; 31 : return 0;
xor eax, eax
; 32 : }
add esp, 16 ; 00000010H
ret 0
_main ENDP
--------------------------------
--------------------------------
小結:
VC++ 6:
當傳送給函數的物件類型與參數類型不符時,會產生為使函數成功調用而建立臨時對象這種情況。條件是僅當通過傳值(by value)方式傳遞對象或傳遞常量引用(reference-to-const)參數時,才會發生這些類型轉換,從而產生臨時對象。
註:如果類型匹配時,即使是通過傳值傳遞對象參數,也不會產生臨時對象,只是將實參拷貝一份壓棧。如:test(a);。
VC++ 2005:
待測。
--------------------------------
b.建立臨時對象的第二種環境是函數返回對象時。例如operator+必須返回一個對象,以表示它的兩個運算元的和
.以某種方法返回對象,能讓編譯器消除臨時對象的開銷,這樣編寫函數通常是很普遍的。這種技巧是返回constructor argument而不是直接返回對象,加上編譯器傳回值最佳化(return value optimization)(RVO)功能,臨時對象就不會被建立。
測試:
--------------------------------
下面看個c++ test program:
//LGVector.h
class LGVector
{
public:
// constructors
LGVector(){}; // make the default constructor be inlined.
//LGVector(const float* pVec);
LGVector(float x, float y, float z);
LGVector(const LGVector& rVec);
// destructor
~LGVector(){}; // this's necessary as VC++6 will turn on RVO if the destructor exists explcitly.
// access methods
// operators
operator= (const LGVector& rVec);
LGVector operator+ (const LGVector& rVec);
private:
float x,y,z;
};
//LGVector.cpp
#include "LGVector.h"
LGVector::LGVector(float x, float y, float z) : x(x), y(y), z(z)
{
}
LGVector::LGVector(const LGVector& rVec)
{
x = rVec.x;
y = rVec.y;
z = rVec.z;
}
LGVector::operator= (const LGVector& rVec)
{
x = rVec.x;
y = rVec.y;
z = rVec.z;
}
/* case1:good style */
LGVector LGVector::operator+ (const LGVector& rVec)
{
return LGVector(x+rVec.x, y+rVec.y, z+rVec.z);
}
/* case2:bad style
LGVector LGVector::operator+ (const LGVector& rVec)
{
LGVector v(x+rVec.x, y+rVec.y, z+rVec.z);
return v;
}
*/
/* case3:bad style
LGVector LGVector::operator+ (const LGVector& rVec)
{
LGVector v = LGVector(x+rVec.x, y+rVec.y, z+rVec.z);
return v;
}
*/
/* case4:bad style
LGVector LGVector::operator+ (const LGVector& rVec)
{
LGVector v;
v = LGVector(x+rVec.x, y+rVec.y, z+rVec.z);
return v;
}
*/
//main.cpp
#include "LGVector.h"
int main(int argc, char* argv[])
{
LGVector v1(1,2,3);
LGVector v2(10,20,30);
LGVector v = v1 + v2;
return 0;
}
下面是LGVector LGVector::operator+ (const LGVector& rVec)的x86 Assembly codes:
(x86, VC++ 6,Release, Optimization: Maximize Speed)
--------------------------------
*********************************************************************************************************
case1:RVO開啟,函數中,只有一個建構函式會調用,沒有額外的拷貝建構函式被調用。如下:
*********************************************************************************************************
_rVec$ = 12
___$ReturnUdt$ = 8
$T287 = -4
??HLGVector@@QAE?AV0@ABV0@@Z PROC NEAR ; LGVector::operator+, COMDAT
; 26 : {
push ecx
; 27 : return LGVector(x+rVec.x, y+rVec.y, z+rVec.z);
mov eax, DWORD PTR _rVec$[esp]
push esi
push ecx
mov esi, DWORD PTR ___$ReturnUdt$[esp+8]
fld DWORD PTR [eax+8]
fadd DWORD PTR [ecx+8]
mov DWORD PTR $T287[esp+12], 0
fstp DWORD PTR [esp]
fld DWORD PTR [eax+4]
fadd DWORD PTR [ecx+4]
push ecx
fstp DWORD PTR [esp]
fld DWORD PTR [eax]
fadd DWORD PTR [ecx]
push ecx
mov ecx, esi
fstp DWORD PTR [esp]
call ??0LGVector@@QAE@MMM@Z ; LGVector::LGVector
mov eax, esi
pop esi
; 28 : }
pop ecx
ret 8
??HLGVector@@QAE?AV0@ABV0@@Z ENDP ; LGVector::operator+
*********************************************************************************************************
case2:函數中,一個建構函式以及一個拷貝建構函式會調用。如下:
*********************************************************************************************************
_rVec$ = 12
___$ReturnUdt$ = 8
_v$ = -12
$T288 = -16
??HLGVector@@QAE?AV0@ABV0@@Z PROC NEAR ; LGVector::operator+, COMDAT
; 33 : {
sub esp, 16 ; 00000010H
; 34 : LGVector v(x+rVec.x, y+rVec.y, z+rVec.z);
mov eax, DWORD PTR _rVec$[esp+12]
push esi
push ecx
mov DWORD PTR $T288[esp+24], 0
fld DWORD PTR [eax+8]
fadd DWORD PTR [ecx+8]
fstp DWORD PTR [esp]
fld DWORD PTR [eax+4]
fadd DWORD PTR [ecx+4]
push ecx
fstp DWORD PTR [esp]
fld DWORD PTR [eax]
fadd DWORD PTR [ecx]
push ecx
lea ecx, DWORD PTR _v$[esp+32]
fstp DWORD PTR [esp]
call ??0LGVector@@QAE@MMM@Z ; LGVector::LGVector
; 35 : return v;
mov esi, DWORD PTR ___$ReturnUdt$[esp+16]
lea eax, DWORD PTR _v$[esp+20]
push eax
mov ecx, esi
call ??0LGVector@@QAE@ABV0@@Z ; LGVector::LGVector
mov eax, esi
pop esi
; 36 : }
add esp, 16 ; 00000010H
ret 8
??HLGVector@@QAE?AV0@ABV0@@Z ENDP ; LGVector::operator+
*********************************************************************************************************
case3:同case2。實際上,LGVector v = LGVector(x+rVec.x, y+rVec.y, z+rVec.z);只會引發建構函式的調用,等同於LGVector v(x+rVec.x, y+rVec.y, z+rVec.z);,不同的寫法而已。多扯一點:會引發拷貝建構函式的調用的是形如LGVector v = v0; 這樣的寫法,而形如v = v0;的寫法會引發operator=調用。如下:
*********************************************************************************************************
_rVec$ = 12
___$ReturnUdt$ = 8
_v$ = -12
$T289 = -16
??HLGVector@@QAE?AV0@ABV0@@Z PROC NEAR ; LGVector::operator+, COMDAT
; 41 : {
sub esp, 16 ; 00000010H
; 42 : LGVector v = LGVector(x+rVec.x, y+rVec.y, z+rVec.z);
mov eax, DWORD PTR _rVec$[esp+12]
push esi
push ecx
mov DWORD PTR $T289[esp+24], 0
fld DWORD PTR [eax+8]
fadd DWORD PTR [ecx+8]
fstp DWORD PTR [esp]
fld DWORD PTR [eax+4]
fadd DWORD PTR [ecx+4]
push ecx
fstp DWORD PTR [esp]
fld DWORD PTR [eax]
fadd DWORD PTR [ecx]
push ecx
lea ecx, DWORD PTR _v$[esp+32]
fstp DWORD PTR [esp]
call ??0LGVector@@QAE@MMM@Z ; LGVector::LGVector
; 43 : return v;
mov esi, DWORD PTR ___$ReturnUdt$[esp+16]
lea eax, DWORD PTR _v$[esp+20]
push eax
mov ecx, esi
call ??0LGVector@@QAE@ABV0@@Z ; LGVector::LGVector
mov eax, esi
pop esi
; 44 : }
add esp, 16 ; 00000010H
ret 8
??HLGVector@@QAE?AV0@ABV0@@Z ENDP ; LGVector::operator+
*********************************************************************************************************
case4:函數中,一個建構函式,一個賦值函數以及一個拷貝建構函式會調用(原本應該是兩個建構函式調用,但是因為LGVector v;引發的建構函式是inline的,所以沒有出現調用),如下:
*********************************************************************************************************
_rVec$ = 12
___$ReturnUdt$ = 8
_v$ = -24
$T288 = -12
$T292 = -28
??HLGVector@@QAE?AV0@ABV0@@Z PROC NEAR ; LGVector::operator+, COMDAT
; 49 : {
sub esp, 28 ; 0000001cH
; 50 : LGVector v;
; 51 : v = LGVector(x+rVec.x, y+rVec.y, z+rVec.z);
mov eax, DWORD PTR _rVec$[esp+24]
push esi
push ecx
mov DWORD PTR $T292[esp+36], 0
fld DWORD PTR [eax+8]
fadd DWORD PTR [ecx+8]
fstp DWORD PTR [esp]
fld DWORD PTR [eax+4]
fadd DWORD PTR [ecx+4]
push ecx
fstp DWORD PTR [esp]
fld DWORD PTR [eax]
fadd DWORD PTR [ecx]
push ecx
lea ecx, DWORD PTR $T288[esp+44]
fstp DWORD PTR [esp]
call ??0LGVector@@QAE@MMM@Z ; LGVector::LGVector
push eax
lea ecx, DWORD PTR _v$[esp+36]
call ??4LGVector@@QAEHABV0@@Z ; LGVector::operator=
; 52 : return v;
mov esi, DWORD PTR ___$ReturnUdt$[esp+28]
lea eax, DWORD PTR _v$[esp+32]
push eax
mov ecx, esi
call ??0LGVector@@QAE@ABV0@@Z ; LGVector::LGVector
mov eax, esi
pop esi
; 53 : }
add esp, 28 ; 0000001cH
ret 8
??HLGVector@@QAE?AV0@ABV0@@Z ENDP ; LGVector::operator+
--------------------------------
--------------------------------
小結:
VC++ 6:
注意編寫我們的代碼,盡量消除臨時對象帶來的開銷。case2,3,4中的額外拷貝建構函式的調用開銷是因為其產生的對象是Named的,不是臨時對象,所以VC++6並不能最佳化掉。
註:定義解構函式得目的是為了讓VC6開啟RVO,從而消除臨時對象的開銷。
VC++2005:
RVO包含兩種,一種是NRVO,另一種是URVO。VC++2005貌似就支援NRVO,也就是說在case2,3,4中也能做到和case1一樣的最佳化效果。
但是需要注意的是URVO一般是不會帶來問題的,NRVO可能會導致不同的行為。
--------------------------------