Improve Performance of C++ Codes (2) -- 如何消除临时对象?


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

你可能感兴趣的:(Improve Performance of C++ Codes (2) -- 如何消除临时对象?)