真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

C++應(yīng)用程序性能優(yōu)化(三)——C++語言特性性能分析

C++應(yīng)用程序性能優(yōu)化(三)——C++語言特性性能分析

一、C++語言特性性能分析簡介

通常大多數(shù)開發(fā)人員認(rèn)為,匯編語言和C語言比較適合編寫對性能要求非常高的程序,C++語言主要適用于編寫復(fù)雜度非常高但性能要求并不是很高的程序。因為大多數(shù)開發(fā)人員認(rèn)為,C++語言設(shè)計時因為考慮到支持多種編程模式(如面向?qū)ο缶幊毯头缎途幊蹋┮约爱惓L幚淼龋瑥亩肓颂嘈碌恼Z言特性。新的語言特性往往使得C++編譯器在編譯程序時插入了很多額外的代碼,會導(dǎo)致最終生成的二進制代碼體積膨脹,而且執(zhí)行速度下降。
但事實并非如此,通常一個程序的速度在框架設(shè)計完成時大致已經(jīng)確定,而并非因為采用C++語言才導(dǎo)致速度沒有達到預(yù)期目標(biāo)。因此,當(dāng)一個程序的性能需要提高時,首先需要做的是用性能檢測工具對其運行的時間分布進行一個準(zhǔn)確的測量,找出關(guān)鍵路徑和真正的性能瓶頸所在,然后針對性能瓶頸進行分析和優(yōu)化,而不是主觀地將性能問題歸咎于程序所采用的語言。工程實踐表明,如果框架設(shè)計不做修改,即使使用C語言或匯編語言重新改寫,也并不能保證提高總體性能。
因此,遇到性能問題時,首先應(yīng)檢查和反思程序的總體架構(gòu),然后使用性能檢測工具對其實際運行做準(zhǔn)確的測量,再針對性能瓶頸進行分析和優(yōu)化。
但C++語言中確實有一些操作、特性比其它因素更容易成為程序的性能瓶頸,常見因素如下:
(1)缺頁
缺頁通常意味著要訪問外部存儲,因為外部存儲訪問相對于訪問內(nèi)存或代碼執(zhí)行,有數(shù)量級的差別。因此,只要有可能,應(yīng)該盡量想辦法減少缺頁。
(2)從堆中動態(tài)申請和釋放內(nèi)存
C語言中的malloc/free和C++語言中的new/delete操作時非常耗時的,因此要盡可能優(yōu)先考慮從線程棧中獲取內(nèi)存。優(yōu)先考慮棧而減少從動態(tài)堆中申請內(nèi)存,不僅因為在堆中分配內(nèi)存比在棧中要慢很多,而且還與盡量減少缺頁有關(guān)。當(dāng)程序執(zhí)行時,當(dāng)前棧幀空間所在的內(nèi)存頁肯定在物理內(nèi)存中,因此程序代碼對其中變量的存取不會引起缺頁;如果從堆空間生成對象,只有指向?qū)ο蟮闹羔樤跅I?,對象本身則存儲在堆空間中。堆一般不可能都在物理內(nèi)存中,而且由于堆分配內(nèi)存的特性,即使兩個相鄰生成的對象,也很有可能在堆內(nèi)存位置上相距很遠。因此,當(dāng)訪問兩個對象時,雖然分別指向兩個對象的指針都在棧上,但通過兩個指針引用對象時很有可能會引起兩次缺頁。
(3)復(fù)雜對象的創(chuàng)建和銷毀
復(fù)雜對象的創(chuàng)建和銷毀會比較耗時,因此對于層次較深的遞歸調(diào)用需要重點關(guān)注遞歸內(nèi)部的對象創(chuàng)建。其次,編譯器生成的臨時對象因為在程序的源碼中看不到,更不容易察覺,因此需要重點關(guān)注。
(4)函數(shù)調(diào)用
由于函數(shù)調(diào)用有固定的額外開銷,因此當(dāng)函數(shù)體的代碼量相對較少,并且函數(shù)被非常頻繁調(diào)用時,函數(shù)調(diào)用時的固定開銷容易成為不必要的開銷。C語言的宏和C++語言的內(nèi)聯(lián)函數(shù)都是為了在保持函數(shù)調(diào)用的模塊化特征基礎(chǔ)上消除函數(shù)調(diào)用的固定額外開銷而引入的。由于C語言的宏在×××能優(yōu)勢的同時也給開發(fā)和調(diào)試帶來不便,因此C++語言中推薦使用內(nèi)聯(lián)函數(shù)。

前進網(wǎng)站制作公司哪家好,找成都創(chuàng)新互聯(lián)公司!從網(wǎng)頁設(shè)計、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、響應(yīng)式網(wǎng)站等網(wǎng)站項目制作,到程序開發(fā),運營維護。成都創(chuàng)新互聯(lián)公司成立與2013年到現(xiàn)在10年的時間,我們擁有了豐富的建站經(jīng)驗和運維經(jīng)驗,來保證我們的工作的順利進行。專注于網(wǎng)站建設(shè)就選成都創(chuàng)新互聯(lián)公司。

二、構(gòu)造函數(shù)與析構(gòu)函數(shù)

1、構(gòu)造函數(shù)與析構(gòu)函數(shù)簡介

構(gòu)造函數(shù)和析構(gòu)函數(shù)的特點是當(dāng)創(chuàng)建對象時自動執(zhí)行構(gòu)造函數(shù);當(dāng)銷毀對象時,析構(gòu)函數(shù)自動被執(zhí)行。構(gòu)造函數(shù)是一個對象最先被執(zhí)行的函數(shù),在創(chuàng)建對象時調(diào)用,用于初始化對象的初始狀態(tài)和取得對象被使用前需要的一些資源,如文件、網(wǎng)絡(luò)連接等;析構(gòu)函數(shù)是一個對象最后被執(zhí)行的函數(shù),用于釋放對象擁有的資源。在對象的生命周期內(nèi),構(gòu)造函數(shù)和析構(gòu)函數(shù)都只會執(zhí)行一次。
創(chuàng)建一個對象有兩種方式,一種是從線程運行棧中創(chuàng)建,稱為局部對象。銷毀局部對象并不需要程序顯示地調(diào)用析構(gòu)函數(shù),而是當(dāng)程序運行出對象所屬的作用域時自動調(diào)用對象的析構(gòu)函數(shù)。
創(chuàng)建對象的另一種方式是從全局堆中動態(tài)分配,通常使用new或malloc分配堆空間。

Obejct* p = new Object();//1
// do something //2
delete p;//3
p = NULL;//4

執(zhí)行語句1時,指針p所指向?qū)ο蟮膬?nèi)存從全局堆空間中獲得,并將地址賦值給p,p本身是一個局部變量,需要從線程棧中分配,p所指向?qū)ο髲娜侄阎蟹峙鋬?nèi)存存放。從全局堆中創(chuàng)建的對象需要顯示調(diào)用delete進行銷毀,delete會調(diào)用指針p指向?qū)ο蟮奈鰳?gòu)函數(shù),并將對象所占的全局堆內(nèi)存空間返回給全局堆。執(zhí)行語句3后,指針p指向的對象被銷毀,但指針p還存在于棧中,直到程序退出其所在作用域。將p指針?biāo)赶驅(qū)ο箐N毀后,p指針仍指向被銷毀對象的全局堆空間位置,此時指針p變成一個懸空指針,此時使用指針p是危險的,通常推薦將p賦值NULL。
在Win32平臺,訪問銷毀對象的全局堆空間內(nèi)存會導(dǎo)致三種情況:
(1)被銷毀對象所在的內(nèi)存頁沒有任何對象,堆管理器已經(jīng)將所占堆空間進一步回收給操作系統(tǒng),此時通過指針訪問會引起訪問違例,即訪問了不合法內(nèi)存,引起進程崩潰。
(2)被銷毀對象所在的內(nèi)存頁存在其它對象,并且被銷毀對象曾經(jīng)占用的全局堆空間被回收后尚未分配給其它對象,此時通過指針p訪問取得的值是無意義的,雖然不會立刻引起進程崩潰,但針對指針p的后續(xù)操作行為是不可預(yù)測的。
(3)被銷毀對象所在的內(nèi)存頁存在其它對象,并且被銷毀對象曾經(jīng)占用的全局堆空間被回收后已經(jīng)分配給其它對象,此時通過指針p取得的值是其它對象,雖然對指針p的訪問不會引起進程崩潰,但極有可能引起對象狀態(tài)的改變。

2、對象的構(gòu)造過程

創(chuàng)建一個對象分為兩個步驟,即首先取得對象所需的內(nèi)存(從線程?;蛉侄眩缓笤趦?nèi)存空間上執(zhí)行構(gòu)造函數(shù)。在構(gòu)造函數(shù)構(gòu)建對象時,構(gòu)造函數(shù)也分為兩個步驟。第一步執(zhí)行初始化(通過初始化參數(shù)列表),第二步執(zhí)行構(gòu)造函數(shù)的函數(shù)體。

class Derived : public Base
{
public:
    Derived(): id(1), name("UnNamed")   // 1
    {
        // do something     // 2
    }
private:
    int id;
    string name;
};

語句1中冒號后的代碼即為初始化列表,每個初始化單元都是變量名(值)的模式,不同單元之間使用逗號分隔。構(gòu)造函數(shù)首先根據(jù)初始化列表執(zhí)行初始化,然后執(zhí)行構(gòu)造函數(shù)的函數(shù)體(語句2)。初始化操作的注意事項如下:
(1)構(gòu)造函數(shù)其實是一個遞歸操作,在每層遞歸內(nèi)部的操作遵循嚴(yán)格的次序。遞歸模式會首先執(zhí)行父類的構(gòu)造函數(shù)(父類的構(gòu)造函數(shù)操作也相應(yīng)包含執(zhí)行初始化和執(zhí)行構(gòu)造函數(shù)函數(shù)體兩個部分),父類構(gòu)造函數(shù)返回后構(gòu)造類自己的成員變量。構(gòu)造類自己的成員變量時,一是嚴(yán)格按照成員變量在類中的聲明順序進行,與成員變量在初始化列表中出現(xiàn)的順序完全無關(guān);二是當(dāng)有些成員變量或父類對象沒有在初始化列表出現(xiàn)時,仍然在初始化操作中對其進行初始化,內(nèi)建類型成員變量被賦值給一個初值,父類對象和類成員變量對象被調(diào)用其默認(rèn)構(gòu)造函數(shù)初始化,然后父類的構(gòu)造函數(shù)和子成員變量對象在構(gòu)造函數(shù)執(zhí)行過程中也遵循上述遞歸操作,直到類的繼承體系中所有父類和父類所含的成員變量都被構(gòu)造完成,類的初始化操作才完成。
(2)父類對象和一些成員變量沒有出現(xiàn)在初始化列表中時,其仍然會被執(zhí)行默認(rèn)構(gòu)造函數(shù)。因此,相應(yīng)對象所屬類必須提供可以調(diào)用的默認(rèn)構(gòu)造函數(shù),為此要求相應(yīng)的類必須顯式提供默認(rèn)構(gòu)造函數(shù),要么不能阻止編譯器隱式生成默認(rèn)構(gòu)造函數(shù),定義除默認(rèn)構(gòu)造函數(shù)外的其它類型的構(gòu)造函數(shù)將會阻止編譯器生成默認(rèn)構(gòu)造函數(shù)。如果編譯器在編譯時,發(fā)現(xiàn)沒有可供調(diào)用的默認(rèn)構(gòu)造函數(shù),并且編譯器也無法生成默認(rèn)構(gòu)造函數(shù),則編譯無法通過。
(3)對兩類成員變量,需要強調(diào)指出(即常量型和引用型)。由于所有成員變量在執(zhí)行函數(shù)體前已經(jīng)被構(gòu)造,即已經(jīng)擁有初始值,因此,對于常量型和引用型變量必須在初始化列表中正確初始化,而不能將其初始化放在構(gòu)造函數(shù)體內(nèi)。
(4)初始化列表可能沒有完全列出其子成員或父類對象成員,或者順序與其在類中的聲明順序不同,仍然會保證嚴(yán)格被全部并且嚴(yán)格按照順序被構(gòu)建。即程序在進入構(gòu)造函數(shù)體前,類的父類對象和所有子成員變量對象已經(jīng)被生成和構(gòu)造。如果在構(gòu)造函數(shù)體內(nèi)為其執(zhí)行賦值操作,顯然屬于浪費。如果在構(gòu)造函數(shù)時已經(jīng)知道如何為類的子成員變量初始化,則應(yīng)該將初始化信息通過構(gòu)造函數(shù)的初始化列表賦予子成員變量,而不是在構(gòu)造函數(shù)體內(nèi)進行初始化,因為進入構(gòu)造函數(shù)時,子成員變量已經(jīng)初始化一次。

3、對象的析構(gòu)過程

析構(gòu)函數(shù)和構(gòu)造函數(shù)一樣,是遞歸的過程,但存在不同。一是析構(gòu)函數(shù)不存在初始化操作部分,析構(gòu)函數(shù)的主要工作就是執(zhí)行析構(gòu)函數(shù)的函數(shù)體;二是析構(gòu)函數(shù)執(zhí)行的遞歸與構(gòu)造函數(shù)相反,在每一層遞歸中,成員變量對象的析構(gòu)順序也與構(gòu)造函數(shù)相反。
析構(gòu)函數(shù)只能選擇類的成員變量在類中聲明的順序作為析構(gòu)的順序參考(正序或逆序)。因為構(gòu)造函數(shù)選擇了正序,而析構(gòu)函數(shù)的工作與構(gòu)造函數(shù)相反,因此析構(gòu)函數(shù)選擇逆序。又因為析構(gòu)函數(shù)只能使用成員變量在類中的聲明順序作為析構(gòu)順序的依據(jù)(正序或逆序),因此構(gòu)造函數(shù)也只能選擇成員變量在類中的聲明順序作為構(gòu)造的順序依據(jù),而不能采用初始化列表的順序作為順序依據(jù)。
如果操作的對象屬于一個復(fù)雜繼承體系的末端節(jié)點,其析構(gòu)過程也將十分耗時。
在C++程序中,創(chuàng)建和銷毀對象是影響性能的一個非常突出的操作。首先,如果是從全局堆空間中生成對象,則需要先進行動態(tài)內(nèi)存分配操作,而動態(tài)內(nèi)存的分配與回收是非常耗時的操作,因為涉及到尋找匹配大小的內(nèi)存塊,找到后可能還需要截斷處理,然后還需要修改維護全局堆內(nèi)存使用情況信息的鏈表。頻繁的內(nèi)存操作會嚴(yán)重影響性能的下降,使用內(nèi)存池技術(shù)可以減少從全局動態(tài)堆空間申請內(nèi)存的次數(shù),提高程序的總體性能。當(dāng)取得內(nèi)存后,如果需要生成的內(nèi)對象屬于復(fù)雜繼承體系的末端類,則構(gòu)造函數(shù)的調(diào)用將會引起一連串的遞歸構(gòu)造操作,在大型復(fù)雜系統(tǒng)中,大量的此類對象構(gòu)造將會消耗CPU操作的主要部分。
由于對象的創(chuàng)建和銷毀會影響性能,在盡量減少自己代碼生成對象的同時,需要關(guān)注編譯器在編譯時臨時生成的對象,盡量避免臨時對象的生成。
如果在實現(xiàn)構(gòu)造函數(shù)時,在構(gòu)造函數(shù)體中進行了第二次的賦值操作,也會浪費CPU時間。

4、函數(shù)參數(shù)傳遞

減少對象創(chuàng)建和銷毀的常見方法是在聲明中將所有的值傳遞改為常量引用傳遞,如:

int func(Object obj);// 1
int func(const Object& obj);// 2

值傳遞驗證示例如下:

#include 

using namespace std;

class Object
{
public:
    Object(int i = 1)
    {
        n = i;
        cout << "Object(int i = 1): " << endl;
    }
    Object(const Object& another)
    {
        n = another.n;
        cout << "Object(const Object& another): " << endl;
    }
    void increase()
    {
        n++;
    }
    int value()const
    {
        return n;
    }
    ~Object()
    {
        cout << "~Object()" << endl;
    }
private:
    int n;
};
void func(Object obj)
{
    cout << "enter func, before increase(), n = " << obj.value() << endl;
    obj.increase();
    cout << "enter func, after increase(), n = " << obj.value() << endl;
}
int main()
{
    Object a;   // 1
    cout << "before call func, n = " << a.value() << endl;
    func(a);    // 2
    cout << "after call func, n = " << a.value() << endl;// 3

    return 0;
}
// output:
//Object(int i = 1):        // 4
//before call func, n = 1
//Object(const Object& another):    // 5
//enter func, before increase(), n = 1  // 6
//enter func, after increase(), n = 2   // 7
//~Object() // 8
//after call func, n = 1    // 9
//~Object()

語句4的輸出為語句1處的對象構(gòu)造,語句5輸出則是語句2處的func(a)函數(shù)調(diào)用,調(diào)用開始時通過拷貝構(gòu)造函數(shù)生成對象a的復(fù)制品,緊跟著在函數(shù)內(nèi)檢查n的輸出值輸出語句6,輸出值與func函數(shù)外部元對象a的值相同,然后復(fù)制品調(diào)用increase函數(shù)將n值加1,此時復(fù)制品的n值為2,并輸出語句7。func函數(shù)執(zhí)行完畢后銷毀復(fù)制品,輸出語句8。main函數(shù)內(nèi)繼續(xù)執(zhí)行,打印原對象a的n值為1,輸出語句9。
當(dāng)函數(shù)需要修改傳入?yún)?shù)時,應(yīng)該用引用傳入?yún)?shù);當(dāng)函數(shù)不會修改傳入?yún)?shù)時,如果函數(shù)聲明中傳入?yún)?shù)為對象,則函數(shù)可以達到設(shè)計目的,但會生成不必要的復(fù)制品對象,從而引入不必要的構(gòu)造和析構(gòu)操作,應(yīng)該使用常量引用傳入?yún)?shù)。
構(gòu)造函數(shù)的重復(fù)賦值對性能影響驗證示例如下:

#include 
#include 

using namespace std;

class DArray
{
public:
    DArray(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
    void init(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
private:
    double d[1000];
};

class Object
{
public:
    Object(double v)
    {
        d.init(v);
    }
private:
    DArray d;
};

int main()
{
    clock_t start, finish;
    start = clock();
    for(int i = 0; i < 100000; i++)
    {
        Object obj(2.0 + i);
    }
    finish = clock();
    cout << "Used Time: " << double(finish - start) << "" << endl;

    return 0;
}

耗時為600000單位,如果通過初始化列表對成員變量進行初始化,其代碼如下:

#include 
#include 

using namespace std;

class DArray
{
public:
    DArray(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
    void init(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
private:
    double d[1000];
};

class Object
{
public:
    Object(double v): d(v)
    {
    }
private:
    DArray d;
};

int main()
{
    clock_t start, finish;
    start = clock();
    for(int i = 0; i < 100000; i++)
    {
        Object obj(2.0 + i);
    }
    finish = clock();
    cout << "Used Time: " << double(finish - start) << "" << endl;

    return 0;
}

耗時為300000單位,性能提高約50%。

三、繼承與虛函數(shù)

1、虛函數(shù)與動態(tài)綁定機制

虛函數(shù)是C++語言引入的一個重要特性,提供了動態(tài)綁定機制,動態(tài)綁定機制使得類繼承的語義變得相對明晰。
(1)基類抽象了通用的數(shù)據(jù)及操作。對于數(shù)據(jù)而言,如果數(shù)據(jù)成員在各個派生類中都需要用到,需要將其聲明在基類中;對于操作而語言,如果操作對于各個派生類都有意義,無論其語義是否會被修改和擴展,需要將其聲明在基類中。
(2)某些操作,對于各個派生類而言,語義完全保持一致,而無需修改和擴展,則相應(yīng)操作聲明為基類的非虛成員函數(shù)。各個派生類在聲明為基類的派生類時,默認(rèn)繼承非虛成員函數(shù)的聲明和實現(xiàn),如果默認(rèn)繼承基類的數(shù)據(jù)成員一樣,而不必另外做任何聲明,構(gòu)成代碼復(fù)用。
(3)對于某些操作,雖然對于各個派生類都有意義,但其語義并不相同,則相應(yīng)的操作應(yīng)該聲明為虛成員函數(shù)。各個派生類雖然也繼承了虛成員函數(shù)的聲明和實現(xiàn),但語義上應(yīng)該對虛成員函數(shù)的實現(xiàn)進行修改或擴展。如果在實現(xiàn)修改、擴展虛成員函數(shù)的過程中,需要用到額外的派生類獨有的數(shù)據(jù)時,則將相應(yīng)的數(shù)據(jù)聲明為派生類自己的數(shù)據(jù)成員。
當(dāng)更高層次的程序框架(繼承體系的使用者)使用此繼承體系時,處理的是抽象層次的對象集合,對象集合的成員本質(zhì)是各種派生類對象,但在處理對象集合的對象時,使用的是抽象層次的操作。高層程序框架并不區(qū)分相應(yīng)操作中哪些操作對于派生類是不變的,哪些操作對于派生類是不同的,當(dāng)實際執(zhí)行到各操作時,運行時系統(tǒng)能夠識別哪些操作需要用到動態(tài)綁定。從而找到對應(yīng)派生類的修改或擴展的操作版本。即對繼承體系的使用者而言,繼承體系內(nèi)部的多樣性是透明的,不必關(guān)心其繼承細節(jié),處理的是一組對使用者而言整體行為一致的對象。即使繼承體系內(nèi)部增加、刪除了某個派生類,或某個派生類的虛函數(shù)實現(xiàn)發(fā)生了改變,使用者的代碼也不必做任何修改,使程序的模塊化程度得到極大提高,其擴展性、維護性和代碼可讀性也會提高。對于對象繼承體系使用者而言,只看到抽象類型,而不必關(guān)心具體是哪種具體類型。

2、虛函數(shù)的效率分析

虛函數(shù)的動態(tài)綁定特性雖然很好,但存在內(nèi)存空間和時間開銷,每個支持虛函數(shù)的類(基類或派生類)都會有一個包含其所有支持的虛函數(shù)的虛函數(shù)表的指針。每個類對象都會隱含一個虛函數(shù)表指針(virtual pointer),指向其所屬類的虛函數(shù)表。當(dāng)通過基類的指針或引用調(diào)用某個虛函數(shù)時,系統(tǒng)需要首先定位指針或引用真正對應(yīng)的對象所隱含的虛函數(shù)指針,然后虛函數(shù)指針根據(jù)虛函數(shù)的名稱對其所指向的虛函數(shù)表進行一個偏移定位,再調(diào)用偏移定位處的函數(shù)指針對應(yīng)的虛函數(shù),即動態(tài)綁定的解析過程。C++規(guī)范只需要編譯器能夠保證動態(tài)綁定的語義,但大多數(shù)編譯器都采用上述方法實現(xiàn)虛函數(shù)。
(1)每個支持虛函數(shù)的類都有一個虛函數(shù)表,虛函數(shù)表的大小與類擁有的虛函數(shù)的多少成正比。一個程序中,每個類的虛函數(shù)表只有一個,與類對象的數(shù)量無關(guān)。支持虛函數(shù)的類的每個類對象都有一個指向類的虛函數(shù)表的虛函數(shù)指針,因此程序運行時虛函數(shù)指針引起的內(nèi)存開銷與生成的類對象數(shù)量成正比。
(2)支持虛函數(shù)的類生成每個對象時,在構(gòu)造函數(shù)中會調(diào)用編譯器在構(gòu)造函數(shù)內(nèi)部插入的初始化代碼,來初始化其虛函數(shù)指針,使其指向正確的虛函數(shù)表。當(dāng)通過指針或引用調(diào)用虛函數(shù)時,會根據(jù)虛函數(shù)指針找到相應(yīng)類的虛函數(shù)表。

3、虛函數(shù)與內(nèi)聯(lián)

內(nèi)聯(lián)函數(shù)通??梢蕴岣叽a執(zhí)行速度,很多普通函數(shù)會根據(jù)情況進行內(nèi)聯(lián)化,但虛函數(shù)無法利用內(nèi)聯(lián)化的優(yōu)勢。因為內(nèi)聯(lián)是在編譯階段編譯器將調(diào)用內(nèi)聯(lián)函數(shù)的位置用內(nèi)聯(lián)函數(shù)體替代(內(nèi)聯(lián)展開),但虛函數(shù)本質(zhì)上是運行期行為。在編譯階段,編譯器無法知道某處的虛函數(shù)調(diào)用在真正執(zhí)行的時后需要調(diào)用哪個具體的實現(xiàn)(即編譯階段無法確定其具體綁定),因此,編譯階段編譯器不會對通過指針或引用調(diào)用的虛函數(shù)進行內(nèi)聯(lián)化。如果需要利用虛函數(shù)的動態(tài)綁定的設(shè)計優(yōu)勢,必須放棄內(nèi)聯(lián)帶來的速度優(yōu)勢。
如果不使用虛函數(shù),可以通過在抽象基類增加一個類型標(biāo)識成員用于在運行時識別具體的派生類對象,在派生類對象構(gòu)造時必須指定具體的類型。繼承體系的使用者調(diào)用函數(shù)時不再需要一次間接地根據(jù)虛函數(shù)表查找虛函數(shù)指針的操作,但在調(diào)用前仍然需要使用switch語句對其類型進行識別。
因此虛函數(shù)的缺點可以認(rèn)為只有兩條,即虛函數(shù)表的空間開銷以及無法利用內(nèi)聯(lián)函數(shù)的速度優(yōu)勢。由于每個含有虛函數(shù)的類在整個程序只有一個虛函數(shù)表,因此虛函數(shù)表引起的空間開銷時非常小的。所以,可以認(rèn)為虛函數(shù)引入的性能缺陷只是無法利用內(nèi)聯(lián)函數(shù)。
通常,非虛函數(shù)的常規(guī)設(shè)計假如需要增加一種新的派生類型,或者刪除一種不再支持的派生類型,都必須修改繼承體系所有使用者的所有與類型相關(guān)的函數(shù)調(diào)用代碼。對于一個復(fù)雜的程序,某個繼承體系的使用者會很多,每次對繼承體系的派生類的修改都會波及使用者。因此,不使用虛函數(shù)的常規(guī)設(shè)計增加了代碼的耦合度,模塊化不強,導(dǎo)致項目的可擴展性、可維護性、代碼可讀性都會降低。面向?qū)ο缶幊痰囊粋€重要目的就是增加程序的可擴展性和可維護性,即當(dāng)程序的業(yè)務(wù)邏輯發(fā)生改變時,對原有程序的修改非常方便,降低因為業(yè)務(wù)邏輯改變而對代碼修改時出錯的概率。
因此,在性能和其它特性的選擇方面,需要開發(fā)人員根據(jù)實際情況進行進行權(quán)衡和取舍,如果性能檢驗確認(rèn)性能瓶頸不是虛函數(shù)沒有利用內(nèi)聯(lián)的優(yōu)勢引起,可以不必考慮虛函數(shù)對性能的影響。

四、臨時對象

1、臨時對象簡介

對象的創(chuàng)建與銷毀對程序的性能影響很大,尤其是對象的類處于一個復(fù)雜繼承體系的末端,或者對象包含很多成員對象(包括其所有父類對象,即直接或者間接父類的所有成員變量對象)時,對程序性能影響尤其顯著。因此,作為一個對性能敏感的程序員,應(yīng)該盡量避免創(chuàng)建不必要的對象,以及隨后的銷毀。除了減少顯式地創(chuàng)建對象,也要盡量避免編譯器隱式地創(chuàng)建對象,即臨時對象。

#include 
#include 

class Matrix
{
public:
    Matrix(double d = 1.0)
    {
        for(int i = 0; i < 10; i++)
        {
            for(int j = 0; j < 10; j++)
            {
                m[i][j] = d;
            }
        }
        cout << "Matrix(double d = 1.0)" << endl;
    }
    Matrix(const Matrix& another)
    {
        cout << "Matrix(const Matrix& another)" << endl;
        memcpy(this, &another, sizeof(another));
    }

    Matrix& operator=(const Matrix& another)
    {
        if(this != &another)
        {
            memcpy(this, &another, sizeof(another));
        }
        cout << "Matrix& operator=(const Matrix& another)" << endl;
        return *this;
    }
    friend const Matrix operator+(const Matrix& m1, const Matrix& m2);
private:
    double m[10][10];
};

const Matrix operator+(const Matrix& m1, const Matrix& m2)
{
    Matrix sum; // 1
    for(int i = 0; i < 10; i++)
    {
        for(int j = 0; j < 10; j++)
        {
            sum.m[i][j] = m1.m[i][j] + m2.m[i][j];
        }
    }
    return sum; // 2
}

int main()
{
    Matrix a(2.0), b(3.0), c; // 3
    c = a + b; // 4
    return 0;
}

由于GCC編譯器默認(rèn)進行了返回值優(yōu)化(Return Value Optimization,簡稱RVO),因此需要指定-fno-elide-constructors選項進行編譯:
g++ -fno-elide-constructors main.cpp
輸出結(jié)果如下:

Matrix(double d = 1.0)      //  1
Matrix(double d = 1.0)      //  2
Matrix(double d = 1.0)      //  3
Matrix(double d = 1.0)      //  4
Matrix(const Matrix& another)   //  5
Matrix& operator=(const Matrix& another)    //  6

分析代碼,語句3生成3個Matrix對象,調(diào)用3次構(gòu)造函數(shù),語句4調(diào)用operator+執(zhí)行到語句1時生成臨時變量sum,調(diào)用1次構(gòu)造函數(shù),語句4調(diào)用賦值操作,不會生成新的Matrix對象。輸出5則是因為a+b調(diào)用operator+函數(shù)時需要返回一個Matrix變量sum,然后進一步通過operator=函數(shù)將sum變量賦值給變量c,但a+b返回時,sum變量已經(jīng)被銷毀,即在operator+函數(shù)調(diào)用結(jié)束時被銷毀,其返回的Matrix變量需要在調(diào)用a+b函數(shù)的棧中開辟空間來存放,臨時的Matrix對象是在a+b返回時通過Matrix拷貝構(gòu)造函數(shù)構(gòu)造,即輸出5打印。
如果使用默認(rèn)GCC編譯選項編譯,GCC編譯器默認(rèn)會進行返回值優(yōu)化。
g++ main.cpp
程序輸出如下:

Matrix(double d = 1.0)
Matrix(double d = 1.0)
Matrix(double d = 1.0)
Matrix(double d = 1.0)
Matrix& operator=(const Matrix& another)

臨時對象與臨時變量并不相同。通常,臨時變量是指為了暫時存放某個值的變量,顯式出現(xiàn)在源碼中;臨時對象通常指編譯器隱式生成的對象。
臨時對象在C++語言中的特征是未出現(xiàn)在源代碼中,而是從棧中產(chǎn)生未命名對象,開發(fā)人員并沒有聲明要使用臨時對象,由編譯器根據(jù)情況產(chǎn)生,通常開發(fā)人員不會注意到其產(chǎn)生。
返回值優(yōu)化(Return Value Optimization,簡稱RVO)是一種優(yōu)化機制,當(dāng)函數(shù)需要返回一個對象的時候,如果自己創(chuàng)建一個臨時對象用戶返回,那么臨時對象會消耗一個構(gòu)造函數(shù)(Constructor)的調(diào)用、一個復(fù)制構(gòu)造函數(shù)的調(diào)用(Copy Constructor)以及一個析構(gòu)函數(shù)(Destructor)的調(diào)用的代價,而如果稍微做一點優(yōu)化,就可以將成本降低到一個構(gòu)造函數(shù)的代價。

2、臨時對象生成

通常,產(chǎn)生臨時對象的場合如下:
(1)當(dāng)實際調(diào)用函數(shù)時傳入的參數(shù)與函數(shù)定義中聲明的變量類型不匹配。
(2)當(dāng)函數(shù)返回一個對象時。
在函數(shù)傳遞參數(shù)為對象時,實際調(diào)用時因為函數(shù)體內(nèi)的對象與實際傳入的對象并不相同,而是傳入對象的拷貝,因此有開發(fā)者認(rèn)為函數(shù)體內(nèi)的拷貝對象也是一個臨時對象,但嚴(yán)格來說,函數(shù)體內(nèi)的拷貝對象并不符合未出現(xiàn)在源碼中。
對于類型不匹配生成臨時對象的情況,示例如下:

#include 

using namespace std;
class Rational
{
public:
    Rational(int a = 0, int b = 1): real(a), imag(b)    // 1
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
private:
    int real;
    int imag;
};

void func()
{
    Rational r;
    r = 100;  // 2
}

int main()
{
    func();
    return 0;
}

執(zhí)行語句2時,由于Rational沒有重載operator=(int i),編譯器會合成一個operator=(const Rational& another)函數(shù),并執(zhí)行逐位拷貝賦值操作,但由于100不是一個Rational對象,但編譯器會盡可能查找合適的轉(zhuǎn)換路徑,以滿足編譯的需要。編譯器發(fā)現(xiàn)存在一個Rational(int a = 0, int b = 1)構(gòu)造函數(shù),編譯器會將語句2右側(cè)的100通過Rational100, 1)生成一個臨時對象,然后用編譯器合成的operator=(const Rational& another)函數(shù)進行逐位賦值,語句2執(zhí)行后,r對象內(nèi)部的real為100,img為1。
C++編譯器為了成功編譯某些語句會生成很多從源碼中不易察覺的輔助函數(shù),甚至對象。C++編譯器提供的自動類型轉(zhuǎn)換確實提高了程序的可讀性,簡化了程序編寫,提高了開發(fā)效率。但類型轉(zhuǎn)換意味著臨時對象的產(chǎn)生,對象的創(chuàng)建和銷毀意味著性能的下降,類型轉(zhuǎn)換還意味著編譯器會生成其它的代碼。因此,如果不需要編譯器提供自動類型轉(zhuǎn)換,可以使用explicit對類的構(gòu)造函數(shù)進行聲明。

#include 

using namespace std;
class Rational
{
public:
    explicit Rational(int a = 0, int b = 1): real(a), imag(b)    // 1
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
private:
    int real;
    int imag;
};

void func()
{
    Rational r; // 2
    r = 100;    // 3
}

int main()
{
    func();
    return 0;
}

此時,進行代碼編譯會報錯:
error: no match for ‘operator=’ (operand types are ‘Rational’ and ‘int’)
錯誤信息提示沒有匹配的operator=函數(shù)將int和Rational對象進行轉(zhuǎn)換。C++編譯器默認(rèn)合成的operator=函數(shù)只接受Rational對象,不能接受int類型作為參數(shù)。要想代碼編譯能夠通過,方法一是提供一個重載的operator=賦值函數(shù),可以接受整型作為參數(shù);方法二是能夠?qū)⒄娃D(zhuǎn)換為Rational對象,然后進一步利用編譯器合成的賦值運算符。將整型轉(zhuǎn)換為Rational對象,可以提供能只傳遞一個整型作為參數(shù)的Rational構(gòu)造函數(shù),考慮到缺省參數(shù),調(diào)用構(gòu)造函數(shù)可能會是無參、一個參數(shù)、兩個參數(shù),此時編譯器可以利用整型變量作為參數(shù)調(diào)用Rational構(gòu)造函數(shù)生成一個臨時對象。由于explicit關(guān)鍵字限定了構(gòu)造函數(shù)只能被顯示調(diào)用,不允許編譯器運用其進行類型轉(zhuǎn)換,此時編譯器不能使用構(gòu)造函數(shù)將整型100轉(zhuǎn)換為Rational對象,所以導(dǎo)致編譯報錯。
通過重載以整型作為參數(shù)的operator=函數(shù)可以成功編譯,代碼如下:

#include 

using namespace std;
class Rational
{
public:
    explicit Rational(int a = 0, int b = 1): real(a), imag(b)    // 1
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
    Rational& operator=(int r)
    {
        real = r;
        imag = 1;
        return *this;
    }
private:
    int real;
    int imag;
};

void func()
{
    Rational r; // 2
    r = 100;    // 3
}

int main()
{
    func();
    return 0;
}

重載operator=函數(shù)后,編譯器可以成功將整型數(shù)轉(zhuǎn)換為Rational對象,同時成功避免了臨時對象產(chǎn)生。
當(dāng)一個函數(shù)返回的是非內(nèi)建類型的對象時,返回結(jié)果對象必須在某個地方存放,編譯器會從調(diào)用相應(yīng)函數(shù)的棧幀中開辟空間,并用返回值作為參數(shù)調(diào)用返回值對象所屬類型的拷貝構(gòu)造函數(shù)在所開辟的空間生成對象,在調(diào)用函數(shù)結(jié)束并返回后可以繼續(xù)利用臨時對象。

#include 
#include 

using namespace std;
class Rational
{
public:
    Rational(int a = 0, int b = 0): real(a), imag(b)
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
    Rational(const Rational& another): real(another.real), imag(another.imag)
    {
        cout << " Rational(const Rational& another)" << endl;
    }
    Rational& operator = (const Rational& other)
    {
        if(this != &other)
        {
            real = other.real;
            imag = other.imag;
        }
        cout << " Rational& operator = (const Rational& other)" << endl;
        return *this;
    }
    friend const Rational operator+(const Rational& a, const Rational& b);
private:
    int real;
    int imag;
};

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    Rational c;
    c.real = a.real + b.real;
    c.imag = a.imag + b.imag;
    cout << " operator+ end" << endl;
    return c; // 2
}

int main()
{
Rational r, a(10, 10), b(5, 8); 
    r = a + b;// 1
    return 0;
}

執(zhí)行語句1時,相當(dāng)于在main函數(shù)中調(diào)用operator+(const Rational& a, const Rational& b)函數(shù),在main函數(shù)的棧中會開辟一塊Rational大小的空間,在operator+(const Rational& a, const Rational& b)函數(shù)內(nèi)部的語句2處,函數(shù)返回使用被銷毀的c對象作為參數(shù)調(diào)用拷貝構(gòu)造函數(shù)在main函數(shù)棧中開辟空間生成一個Rational對象。然后使用operator =執(zhí)行賦值操作。編譯如下:
g++ -fno-elide-constructors main.cpp
輸出如下:

 Rational(int a = 0, int b = 0)
 Rational(int a = 0, int b = 0)
 Rational(int a = 0, int b = 0)
 operator+ begin
 Rational(int a = 0, int b = 0)
 operator+ end
 Rational(const Rational& another)
 Rational& operator = (const Rational& other)

由于r對象在默認(rèn)構(gòu)造后并沒有使用,可以延遲生成,代碼如下:

#include 
#include 

using namespace std;
class Rational
{
public:
    Rational(int a = 0, int b = 0): real(a), imag(b)
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
    Rational(const Rational& another): real(another.real), imag(another.imag)
    {
        cout << " Rational(const Rational& another)" << endl;
    }
    Rational& operator = (const Rational& other)
    {
        if(this != &other)
        {
            real = other.real;
            imag = other.imag;
        }
        cout << " Rational& operator = (const Rational& other)" << endl;
        return *this;
    }
    friend const Rational operator+(const Rational& a, const Rational& b);
private:
    int real;
    int imag;
};

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    Rational c;
    c.real = a.real + b.real;
    c.imag = a.imag + b.imag;
    cout << " operator+ end" << endl;
    return c; // 2
}

int main()
{
    Rational a(10, 10), b(5, 8);
    Rational r = a + b;  // 1
    return 0;
}

編譯過程如下:
g++ -fno-elide-constructors main.cpp
輸出如下:

 Rational(int a = 0, int b = 0)
 Rational(int a = 0, int b = 0)
 operator+ begin
 Rational(int a = 0, int b = 0)
 operator+ end
 Rational(const Rational& another)
 Rational(const Rational& another)

分析代碼,編譯器執(zhí)行語句1時語義發(fā)生了較大變化,編譯器對=的解釋不再是賦值操作符,而是對象r的初始化。在取得a+b的結(jié)果時,在main函數(shù)棧中開辟空間,使用c對象作為參數(shù)調(diào)用拷貝構(gòu)造函數(shù)生成一個臨時對象,然后使用臨時對象作為參數(shù)調(diào)用拷貝構(gòu)造函數(shù)生成r對象。
因此,對于非內(nèi)建對象,盡量將對象延遲到確切直到其有效狀態(tài)時,可以有效減少臨時對象生成。如將Rational r;r = a + b;改寫為Rational r = a + b;
進一步,可以將operator+函數(shù)改寫為如下:

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    return Rational(a.real + b.real, a.imag + b.imag); // 2
}

通常,operator+與operator+=需要以其實現(xiàn),Rational的operator+=實現(xiàn)如下:

Rational operator+=(const Rational& a)
    {
        real += a.real;
        imag = a.imag;
        return *this;
    }

operator+=沒有產(chǎn)生臨時對象,盡量用operator+=代替operator+操作??紤]到代碼復(fù)用性,operator+可以使用operator+=實現(xiàn),代碼如下:

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    return Rational(a) += b; // 2
}

對于前自增操作符實現(xiàn)如下:

const Rational operator++()
{
        ++real;
        return *this;
}

對于后自增操作如下:

const Rational operator++(int)
{
        Rational temp(*this);
        ++(*this);
        return temp;
}

前自增只需要將自身返回,后自增需要返回一個對象,因此需要多生成兩個對象:函數(shù)體內(nèi)的局部變量和臨時對象,因此對于非內(nèi)建類型,在保證程序語義下盡量使用前自增。

3、臨時對象的生命周期

C++規(guī)范中定義了臨時對象的生命周期從創(chuàng)建時開始,到包含創(chuàng)建它的最長語句執(zhí)行完畢。

string a, b;
const char* str;
if(strlen(str = (a + b).c_str()) > 5) // 1
{
    printf("%s\n", str);// 2
}

分析代碼,語句1處首先創(chuàng)建一個臨時對象存放a+b的值,然后將臨時對象的內(nèi)容通過c_str函數(shù)得到賦值給str,如果str長度大于5則執(zhí)行語句2,但臨時對象生命周期在包含其創(chuàng)建的最長語句已經(jīng)結(jié)束,當(dāng)進入if語句塊時,臨時對象已經(jīng)被銷毀,執(zhí)行其內(nèi)部字符串的str指向的是一段已經(jīng)回收的內(nèi)存,結(jié)果是無法預(yù)測的。但存在一個特例,當(dāng)用一個臨時對象來初始化一個常量引用時,臨時對象的生命周期會持續(xù)到與綁定其上的常用引用銷毀時。示例代碼如下:

string a, b;
if(true)
{
    const string& c = a + b; // 1

}

語句1將a+b結(jié)果的臨時對象綁定到常量引用c,臨時對象生命周期會持續(xù)到c的作用域結(jié)束,不會在語句1結(jié)束時結(jié)束。

五、內(nèi)聯(lián)函數(shù)

1、C++內(nèi)聯(lián)函數(shù)簡介

C++語言的設(shè)計中,內(nèi)聯(lián)函數(shù)的引入完全是為了性能的考慮,因此在編寫對性能要求較高的C++程序時,極有必要考量內(nèi)聯(lián)函數(shù)的使用。
內(nèi)聯(lián)是將被調(diào)用函數(shù)的函數(shù)體代碼直接地整個插入到函數(shù)被調(diào)用處,而不是通過call語句進行。C++編譯器在真正進行內(nèi)聯(lián)時,由于考慮到被內(nèi)聯(lián)函數(shù)的傳入?yún)?shù)、自己的局部變量以及返回值的因素,不只進行簡單的代碼拷貝,還有許多細致工作。

2、C++函數(shù)內(nèi)聯(lián)的聲明

開發(fā)人員可以有兩種方法告訴C++編譯器需要內(nèi)聯(lián)哪些類成員函數(shù),一種是在類的定義體外,一種是在類的定義體內(nèi)。
(1)在類的定義體外時,需要在類成員函數(shù)的定義前加inline關(guān)鍵字,顯式地告訴C++編譯器本函數(shù)在調(diào)用時需要內(nèi)聯(lián)處理。

class Student
{
public:
    void setName(const QString& name);
    QString getName()const;
    void setAge(const int age);
    getAge()const;
private:
    QString m_name;
    int m_age;
};

inline void Student::setName(const QString& name)
{
    m_name = name;
}
inline QString Student::getName()const
{
    return m_name;
}
inline void Student::setAge(const int age)
{
    m_age = age;
}
inline Student::getAge()const
{
    return m_age;
}

(2)在類的定義體內(nèi)且聲明成員函數(shù)時,同時提供類成員函數(shù)的實現(xiàn)體。此時,inline關(guān)鍵字不是必須的。

class Student
{
public:
    void setName(const QString& name)
    {
        m_name = name;
    }
    inline QString getName()const
    {
        return m_name;
    }
    inline void setAge(const int age)
    {
        m_age = age;
    }
    inline getAge()const
    {
        return m_age;
    }
private:
    QString m_name;
    int m_age;
};

(3)普通函數(shù)(非類成員函數(shù))需要被內(nèi)聯(lián)時,需要在普通函數(shù)的定義前加inline關(guān)鍵字,顯式地告訴C++編譯器本函數(shù)在調(diào)用時需要內(nèi)聯(lián)處理。

inline int add(int a, int b)
{
    return a + b;
}

3、C++內(nèi)聯(lián)機制

C++是以編譯單元為單位編譯的,通常一個編譯單元基本等同于一個CPP文件。在編譯的預(yù)處理階段,預(yù)處理器會將#include的各個頭文件(支持遞歸頭文件展開)完整地復(fù)制到CPP文件的對應(yīng)位置處,并進行宏展開等操作。預(yù)處理器處理后,編譯才真正開始。一旦C++編譯器開始編譯,C++編譯器將不會意識到其它CPP文件的存在,因此并不會參考其它CPP文件的內(nèi)容信息。因此,在編譯某個編譯單元時,如果本編譯單元會調(diào)用到某個內(nèi)聯(lián)函數(shù),那么內(nèi)聯(lián)函數(shù)的函數(shù)定義(函數(shù)體)必須包含在編譯單元內(nèi)。因為C++編譯器在使用內(nèi)聯(lián)函數(shù)體代碼替換內(nèi)聯(lián)函數(shù)調(diào)用時,必須知道內(nèi)聯(lián)函數(shù)的函數(shù)體代碼,并且不能通過參考其它編譯單元信息獲得。
如果多個編譯單元會用到同一個內(nèi)聯(lián)函數(shù),C++規(guī)范要求在多個編譯單元中同一個內(nèi)聯(lián)函數(shù)的定義必須是完全一致的,即ODR(One Definition Rule)原則??紤]到代碼的可維護性,通常將內(nèi)聯(lián)函數(shù)的定義放在一個頭文件中,用到內(nèi)聯(lián)函數(shù)的所有編譯單元只需要#include相應(yīng)的頭文件即可。

#include 
#include 

using namespace std;
class Student
{
public:
    void setName(const string& name)
    {
        m_name = name;
    }
    inline string getName()const
    {
        return m_name;
    }
    inline void setAge(const int age)
    {
        m_age = age;
    }
    inline int getAge()const
    {
        return m_age;
    }
private:
    string m_name;
    int m_age;
};

void Print()
{
    Student s;
    s.setAge(20);
    cout << s.getAge() << endl;
}

int main()
{
    Print();
    return 0;
}

上述代碼中,在不開啟內(nèi)聯(lián)時調(diào)用函數(shù)Print的函數(shù)時相關(guān)的操作如下:
(1)進入Print函數(shù)時,從其棧幀中開辟了放置s對象的空間。
(2)進入函數(shù)體后,首先在開辟的s對象存儲空間執(zhí)行Student的默認(rèn)構(gòu)造函數(shù)構(gòu)造s對象。
(3)將常數(shù)20壓棧,調(diào)用s的setAge函數(shù)(開辟setAge函數(shù)的棧幀,返回時回退銷毀此棧幀).
(4)執(zhí)行s的getAge函數(shù),并將返回值壓棧.
(5)調(diào)用cout操作符操作壓棧的結(jié)果,即輸出。
開啟內(nèi)聯(lián)后,Print函數(shù)的等效代碼如下:

void Print()
{
    Student s;
    {
        s.m_age = 20;
    }
    int tmp = s.m_age;
    cout << tmp << endl;
}

函數(shù)調(diào)用時的參數(shù)壓棧,棧幀開辟與銷毀等操作不再需要,結(jié)合內(nèi)聯(lián)后代碼,編譯器會進一步優(yōu)化為如下結(jié)果:

int main()
{
    cout << 20 << endl;
    return 0;
}

如果不考慮setAge/getAge函數(shù)內(nèi)聯(lián),對于非內(nèi)聯(lián)函數(shù)一般不會在頭文件中定義,因此setAge/getAge函數(shù)可能在本編譯單元之外的其它編譯單元定義,Print函數(shù)所在的編譯單元會看不到setAge/getAge,不知道函數(shù)體的具體代碼信息,不能作出進一步的代碼優(yōu)化。
因此,函數(shù)內(nèi)聯(lián)的優(yōu)點如下:
(1)減少因為函數(shù)調(diào)用引起的開銷,主要是參數(shù)壓棧、棧幀開辟與回收、寄存器保存與恢復(fù)。
(2)內(nèi)聯(lián)后編譯器在處理調(diào)用內(nèi)聯(lián)函數(shù)的函數(shù)時,因為可供分析的代碼更多,因此編譯器能做的優(yōu)化更深入徹底。
程序的唯一入口main函數(shù)肯定不會被內(nèi)聯(lián)化,編譯器合成的默認(rèn)構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、析構(gòu)函數(shù)以及賦值運算符一般都會被內(nèi)聯(lián)化。編譯器并不保證使用inline修飾的函數(shù)在編譯時真正被內(nèi)聯(lián)處理,inline只是給編譯器的建議,編譯其完全會根據(jù)實際情況對其忽視。

4、函數(shù)調(diào)用機制

int add(int a, int b)
{
    return a + b;
}

void func()
{
    ...
    int c = add(a, b);
    ...
}

函數(shù)調(diào)用時相關(guān)操作如下:
(1)參數(shù)壓棧
參數(shù)是a,b;壓棧時通常按照逆序壓棧,因此是b,a;如果參數(shù)中有對象,需要先進行拷貝構(gòu)造。
(2)保存返回地址
即函數(shù)調(diào)用結(jié)束后接著執(zhí)行的語句的地址。
(3)保存維護add函數(shù)棧幀信息的寄存器內(nèi)容,如SP(對棧指針),F(xiàn)P(棧棧指針)等。具體保存的寄存器與硬件平臺有關(guān)。
(4)保存某些通用寄存器的內(nèi)容。由于某些通用寄存器會被所有函數(shù)用到,所以在func函數(shù)調(diào)用add之前,這些通用寄存器可能已經(jīng)存儲了對func有用的信息。但這些通用寄存器在進入add函數(shù)體內(nèi)執(zhí)行時可能會被add函數(shù)用到,從而被覆寫。因此,func函數(shù)會在調(diào)用add函數(shù)前保存一份這些通用寄存器的內(nèi)容,在add函數(shù)返回后恢復(fù)。
(5)調(diào)用add函數(shù)。首先通過移動棧指針來分配所有在其內(nèi)部聲明的局部變量所需的空間,然后執(zhí)行其函數(shù)體內(nèi)的代碼。
(6)add函數(shù)執(zhí)行完畢,函數(shù)返回時,func函數(shù)需要進行善后處理,如恢復(fù)通用寄存器的值,恢復(fù)保存func函數(shù)棧幀信息的寄存器的值,通過移動棧指針銷毀add函數(shù)的棧幀,將保存的返回地址出棧并賦值給IP寄存器,通過移動棧指針回收傳給add函數(shù)的參數(shù)所占的空間。
如果函數(shù)的傳入?yún)?shù)和返回值都為對象時,會涉及對象的構(gòu)造與析構(gòu),函數(shù)調(diào)用的開銷會更大。

5、內(nèi)聯(lián)的效率分析

因為函數(shù)調(diào)用的準(zhǔn)備與善后工作最終都由機器指令完成,假設(shè)一個函數(shù)之前的準(zhǔn)備工作與之后的善后工作的指令所需的空間為SS,執(zhí)行指令所需的時間為TS,從時間和空間分析內(nèi)聯(lián)的效率如下:
(1)空間效率。通常認(rèn)為,如果不采用內(nèi)聯(lián),被調(diào)用函數(shù)代碼只有一份,在調(diào)用位置使用call語句即可。而采用內(nèi)聯(lián)后,被調(diào)用函數(shù)的代碼在所調(diào)用的位置都會有一份拷貝,因此會導(dǎo)致代碼膨脹。
如果函數(shù)func的函數(shù)體代碼為FuncS,假設(shè)func函數(shù)在整個程序內(nèi)被調(diào)用n次,不采用內(nèi)聯(lián)時,對func函數(shù)的調(diào)用只有準(zhǔn)備工作與善后工作會增加最后的代碼量開銷,func函數(shù)相關(guān)的代碼大小為n*SS + FuncS。采用內(nèi)聯(lián)后,在各個函數(shù)調(diào)用位置都需要將函數(shù)體代碼展開,即func函數(shù)的相關(guān)代碼大小為n*FuncS。所以需要比較
n*SS + FuncSn*FuncS的大小,如果調(diào)用次數(shù)n較大,可以簡化為比較SS與FuncS的大小。如果內(nèi)聯(lián)函數(shù)自己的函數(shù)體代碼量比因為函數(shù)調(diào)用的準(zhǔn)備與善后工作引入的代碼量大,則內(nèi)聯(lián)后程序的代碼量會變大;如果內(nèi)聯(lián)函數(shù)自己的函數(shù)體代碼量比因為函數(shù)調(diào)用的準(zhǔn)備與善后工作引入的代碼量小,則內(nèi)聯(lián)后程序的代碼量會變小;如果內(nèi)聯(lián)后編譯器因為獲得更多的代碼信息,從而對調(diào)用函數(shù)的優(yōu)化更深入徹底,則最終的代碼量會更小。
(2)時間效率。通常,內(nèi)聯(lián)后函數(shù)調(diào)用都不再需要做函數(shù)調(diào)用的準(zhǔn)備與善后工作,并且由于編譯器可以獲得更多的代碼信息,可以進行深入徹底的代碼優(yōu)化。內(nèi)聯(lián)后,調(diào)用函體內(nèi)需要執(zhí)行的代碼是相鄰的,其執(zhí)行的代碼都在同一個頁面或連續(xù)的頁面中。如果沒有內(nèi)聯(lián),執(zhí)行到被調(diào)用函數(shù)時,需要調(diào)轉(zhuǎn)到包含被調(diào)用函數(shù)的內(nèi)存頁面中執(zhí)行,而被調(diào)用函數(shù)的所屬的頁面極有可能當(dāng)時不在物理內(nèi)存中。因此,內(nèi)聯(lián)后可以降低缺頁的概率,減少缺頁次數(shù)的效果遠比減少一些代碼量執(zhí)行的效果要好。即使被調(diào)用函數(shù)所在頁面也在內(nèi)存中,但與調(diào)用函數(shù)在空間上相隔甚遠,可能會引起cache miss,從而降低執(zhí)行速度。因此,內(nèi)聯(lián)后程序的執(zhí)行時間會比沒有內(nèi)聯(lián)要少,即程序執(zhí)行速度會更快。但如果FunS遠大于SS,且n較大,最終程序的大小會比沒有內(nèi)聯(lián)大的多,用來存放代碼的內(nèi)存頁也會更多,導(dǎo)致執(zhí)行代碼引起的缺頁也會增多,此時,最終程序的執(zhí)行時間可能會因為大量的缺頁變得更多,即程序變慢。因此,很多編譯器會對函數(shù)體代碼很多的函數(shù)拒絕其內(nèi)聯(lián)請求,即忽略inine關(guān)鍵字,按照非內(nèi)聯(lián)函數(shù)進行編譯。
因此,是否采用內(nèi)聯(lián)時需要根據(jù)內(nèi)聯(lián)函數(shù)的特征(如函數(shù)體代碼量、程序被調(diào)用次數(shù)等)進行判斷。判斷內(nèi)聯(lián)效果的最終和最有效方法還是對程序執(zhí)行速度和程序大小進行測量,然后根據(jù)測量結(jié)果決定是否采用內(nèi)聯(lián)和對哪些函數(shù)進行內(nèi)聯(lián)。

6、內(nèi)聯(lián)函數(shù)的二進制兼容問題

調(diào)用內(nèi)聯(lián)函數(shù)的編譯單元必須具有內(nèi)聯(lián)函數(shù)的函數(shù)體代碼信息,考慮到ODR規(guī)則和代碼可維護性,通常將內(nèi)聯(lián)函數(shù)的定義放在頭文件中,每個調(diào)用內(nèi)聯(lián)函數(shù)的編譯單元通過#include相應(yīng)頭文件。
在大型軟件中,某個內(nèi)聯(lián)函數(shù)因為比較通用,可能會被大多數(shù)編譯單元用到,如果對內(nèi)聯(lián)函數(shù)進行修改會引起所有用到該內(nèi)聯(lián)函數(shù)的編譯單元進行重新編譯。對于大型程序,重新編譯大部分編譯單元會消耗大量的編譯時間,因此,內(nèi)聯(lián)函數(shù)最好在開發(fā)的后期引入,以避免可能不必要的大量編譯時間浪費。
如果某開發(fā)組使用了第三方提供的程序庫,而第三方程序庫中可能包含內(nèi)聯(lián)函數(shù),因此在開發(fā)組代碼中使用了第三方庫的內(nèi)聯(lián)函數(shù)位置都會將內(nèi)聯(lián)函數(shù)體代碼拷貝到函數(shù)調(diào)用位置。如果第三方庫提供商在下一個版本中修改了某些內(nèi)聯(lián)函數(shù)的定義,即使沒有修改任何函數(shù)的對外接口,開發(fā)組想要使用新版本的第三方庫仍然需要重新編譯。如果程序已經(jīng)發(fā)布,則重新編譯的成本會極高。如果沒有內(nèi)聯(lián),第三方庫提供商只是修改了函數(shù)實現(xiàn),開發(fā)組不必重新編譯即可使用最新的第三方庫版本。

7、遞歸函數(shù)的內(nèi)聯(lián)

內(nèi)聯(lián)的本質(zhì)是使用函數(shù)體代碼對函數(shù)調(diào)用進行替換,對于遞歸函數(shù):

int sum(int n)
{
    if(n < 2)
    {
        return 1;
    }
    else
    {
        return sum(n - 1) + n;
    }
}

如果某個編譯單元內(nèi)調(diào)用了sum函數(shù),如下:

void func()
{
    ...
    int ret = sum(n);
    ...
}

如果在編譯本編譯單元且調(diào)用sum函數(shù)時,提供的參數(shù)n不能夠知道實際值,則編譯器無法知道對sum函數(shù)進行了多少次替換,編譯器會拒絕對遞歸函數(shù)sum進行內(nèi)聯(lián);如果在編譯本編譯單元且調(diào)用sum函數(shù)時,提供的參數(shù)n可以知道實際值,則編譯器可能會根據(jù)n的大小來判斷時都對sum函數(shù)進行內(nèi)聯(lián),如果n很大,內(nèi)聯(lián)展開可能會使最終程序的大小變得很大。

8、虛函數(shù)的內(nèi)聯(lián)

內(nèi)聯(lián)函數(shù)是編譯階段的行為,虛函數(shù)是執(zhí)行階段行為,因此編譯器一般會拒絕對虛函數(shù)進行內(nèi)聯(lián)的請求。虛函數(shù)不能被內(nèi)聯(lián)是由于編譯器在編譯時無法知道調(diào)用的虛函數(shù)到底是哪一個版本,即無法確定虛函數(shù)的函數(shù)體,但在兩種情況下,編譯器能夠知道虛函數(shù)調(diào)用的真實版本,因此可以內(nèi)聯(lián)。
一是通過對象而不是指向?qū)ο蟮闹羔樆蛞脤μ摵瘮?shù)進行調(diào)用,此時編譯器在編譯器已經(jīng)知道對象的確切類型,因此會直接調(diào)用確切類型的虛函數(shù)的實現(xiàn)版本,而不會產(chǎn)生動態(tài)綁定行為的代碼。
二是雖然通過對象指針或?qū)ο笠谜{(diào)用虛函數(shù),但編譯器在編譯時能夠知道指針或引用指向?qū)ο蟮拇_切類型,如在產(chǎn)生新對象時做的指針賦值或引用初始化與通過指針或引用調(diào)用虛函數(shù)處于同一編譯單元,并且指針沒有被改變賦值使其指向到其它不能知道確切類型的對象,此時編譯器也不會產(chǎn)生動態(tài)綁定的代碼,而是直接調(diào)用確切類型的虛函數(shù)實現(xiàn)版本。

inline virtual int x::y(char* a)
{
    ...
}

void func(char* b)
{
    x_base* px = new x();
    x ox;
    px->y(b);
    ox.y(b);
}

9、C++內(nèi)聯(lián)與C語言宏的區(qū)別

C語言宏與C++內(nèi)聯(lián)的區(qū)別如下:
(1)C++內(nèi)聯(lián)是編譯階段行為,宏是預(yù)處理行為,宏的替代展開由預(yù)處理器負(fù)責(zé),宏對于編譯器是不可見的。
(2)預(yù)處理器不能對宏的參數(shù)進行類型檢查,編譯器會對內(nèi)聯(lián)函數(shù)的參數(shù)進行類型檢查。
(3)宏的參數(shù)在宏體內(nèi)出現(xiàn)兩次以上時通常會產(chǎn)生副作用,尤其是當(dāng)在宏體內(nèi)對參數(shù)進行自增、自減操作時,內(nèi)聯(lián)不會。
(4)宏肯定會被展開,inline修飾的函數(shù)不一定會被內(nèi)聯(lián)展開。


網(wǎng)頁名稱:C++應(yīng)用程序性能優(yōu)化(三)——C++語言特性性能分析
分享鏈接:http://weahome.cn/article/jhisge.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部