条款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可能会导致不同的行为。
--------------------------------