Improve Performance of C++ Codes (2) — 如何消除臨時對象?

來源:互聯網
上載者:User
條款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可能會導致不同的行為。
--------------------------------

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.