《C++应用程序性能优化::第二章C++语言特性的性能分析》学习和理解
《C++應用程序性能優化::第二章C++語言特性的性能分析》學習和理解
說明:《C++應用程序性能優化》 作者:馮宏華等 2007年版。最近出了新版,看了目錄,在前面增加了一章的內容,其它的沒變。
知識點:分析可能引起性能下降的幾個方面:構造函數/析構函數,繼承與虛擬,臨時對象,內聯函數
1、性能瓶頸
很多時候,一個程序的速度在框架設計完成時大致已經確定了,而并非是因為采用了C++語言才使其速度沒有達到預期的目標。當遇到性能問題時,首先應該檢查和反思程序的總體框架。然后用性能檢測工具對其實際運行做準確地測量,再針對瓶頸進行分析和優化,這才是正確的思路。
確實有一些操作或者C++的一些語言特性比其它因素更容易成為程序的瓶頸,一般公認的有如下因素:
(1)?????? 缺頁。缺頁往往意味著需要訪問外部存儲。因為外部存儲訪問相對于訪問內存或者代碼執行,有數量級的差別。只要有可能,應該盡量減少缺頁。
(2)?????? 從堆中動態申請和釋放內存:new/malloc,delete/free都是非常耗時的,因此應該盡可能優先考慮從線程棧中獲得內存。優先考慮棧而減少從動態堆中申請內存,不止因為在堆中開辟內存比棧慢,還因為要盡量減少缺頁。當程序執行時,當前??臻g所在的內存頁肯定在物理內存中,因此不會出現缺頁;而訪問堆中的對象則可能會引起缺頁。
(3)?????? 復雜對象的創建和銷毀。這往往是一個層次相當深的遞歸調用,可能會有臨時對象在不知不覺中產生。
(4)?????? 函數調用。函數調用有固定的額外開銷,當函數體代碼量較少時應該采用C++的內聯函數。
2、構造函數和析構函數
創建一個對象一般有兩種式:一種是從線程運行棧中創建,也稱為“局部對象”:ClassA obj;;一種是從全局堆中動態創建:ClassA *p = new ClassA;…delete p;。
局部對象空間的分配在程序進入該對象的作用域時就已經分配好了,一般是通過移動棧指針,當執行到ClassA obj;時,只需要調用構造函數。
動態創建對象,執行Class *p = new ClassA;時會從全局堆中獲取對象內存空間,并將地址指針賦值該p。p是棧中的局部對象,p所指向的對象是全局堆中的空間。在全局堆中要銷毀對象必須顯式調用delete,delete p會調用p所指向對象的析構函數,并將該對象所占的全局堆內存返回給全局堆。
銷毀了對象之后,p指針在程序退出p所在作用域之前還是存在于棧中的,此段時間內p指針稱為“懸垂指針”或“野指針”,p仍然指向被銷毀的對象的位置。在這段時間內,在win32平臺中,訪問p指針會出現3種情況:
?、?第1種情況是該處位置所在的“內存頁”沒有任何對象,堆管理器已經將其進一步返回給系統,這種錯誤會導致進程崩潰;
②?第2種情況是該處位置所在的“內存頁”還有其他對象,且該處位置被回收后,尚未被分配出去,這時通過指針指針p訪問該處內存,取得的值是無意義的,雖然不會立即引起進程崩潰,但是針對該指針的后續操作的行為是不可預測的;
?、?第3種情況是該處位置所在的“內存頁”還有其他對象,且該處位置被回收后,已被其他對象申請,這時通過指針p訪問該處內存,取得的值其實是程序其他處生成的對象。
創建一個對象分成兩個步驟:首先取得對象所需的內存(無論是從線程棧還是從全局堆中),然后在該塊內存上執行構造函數。在構造函數構建該對象時,構造函數也分成兩個步驟:第一步執行初始化(通過初始化列表),第二步執行構造函數的函數體。(代碼舉例見后文)
對初始化操作有幾點需要注意:
?、?構造函數其實是一個遞歸操作,每層遞歸內部的操作遵循嚴格的次序。遞歸模式為首先執行父類的構造函數,父類構造函數返回后構造該類自己的成員變量。構造該類的成員變量,一是嚴格按照成員變量在類中的聲明順序進行,而與其在初始化列表中出現的順序完全無關;二是當有些成員變量或父類對象沒有在初始化列表中出現時,它們仍然在初始化操作這一步驟中被初始化。
② 父類對象和一些成員變量沒有出現在初始化列表中時,這些對象仍然被執行構造函數,這時執行的是“默認構造函數”。因此這些對象所屬的類必須提供可以調用的默認構造函數,為此要求這些類要么自己“顯式”地提供默認構造函數,要么不能阻止編譯器“隱式”地為其生成一個默認構造函數,定義除默認構造函數之外的其他類型的構造函數就會阻止編譯器生成默認構造函數。
?、?對兩類成員變量:“常量”(const)和“引用”(reference)。因為所有成員變量在執行構造函數函數體之前已經被構造,即已經擁有初值。所以“常量”和“引用”類型必須在初始化列表中初始化,而不能將其初始化放在構造函數函數體內。
?、?在程序進入構造函數函數體之前,類的父類對象和所有子成員變量對象已經被生成和構造。如果在構造函數體內為其執行賦值操作,顯示屬于浪費。如果在構造函數時已經知道如何為類的子成員變量初始化,那么應該將這些初始化信息通過構造函數的初始化列表賦予子成員變量,而不是在構造函數函數體中進行這些初始化。因為進入構造函數函數體之前,這些子成員變量已經初始化過一次了。
在C++程序中,創建/銷毀對象是影響性能的一個非常突出的操作。首先,如果是從全局堆中生成對象,則需要首先進行動態內存分配操作。眾所周知,動態分配/回收在C/C++程序中一直都是非常耗時的。因為牽涉到尋找匹配大小的內存塊,找到后可能還需要截斷處理,然后還需要修改維護全局堆內存使用情況信息的鏈表等。因為意識到頻繁的內存操作會嚴重影響性能,所以已經發展出很多技術用來緩解和降低這種影響,例如內存池技術。
?????? 盡量少使用值傳遞,而使用常量引用傳遞。
3、繼承與虛擬函數
虛擬函數是C++語言引入的一個很重要的特性,它提供了“動態綁定”機制,正是這一機制使得繼承的語義變得相對清晰。在繼承體系中如何聲明操作和變量:
① 基類抽象了通用的數據及操作,就數據而言,如果該數據成員在個派生類中都需要用到,那么就需要將其聲明在基類中;就操作而言,如果該操作對各派生類都有意義,無論其語義是否會被修改或擴展,都需要將其聲明在基類中。
?、?有些操作,如果對于各個派生類而言,語義保持完成一致,而無需修改或擴展,那么這些操作就應該聲明為基類的非虛擬成員函數。各派生類默認繼承了這些非虛擬函數的聲明/實現,如同默認繼承基類的數據成員一樣,而不必另外做任何聲明,這就是繼承帶來的代碼重用的優點。
?、?另外還有一些操作,對于各派生類而言都有意義,但是其語義(實現)并不相同。這時,這些操作應該聲明為基類的虛擬成員函數。各派生類雖然也默認繼承了這些虛擬成員函數的聲明/實現,但是語義上它們應該對這些虛擬成員函數的實現進行修改或擴展。另外在實現這些修改或擴展過程中,需要用到額外的該派生類獨有的數據時,將這些數據聲明為此派生類自己的數據成員。
再考慮使用者對這個繼承體系的使用是如何實現多態、模塊化的。
當更高層次的程序框架(繼承體系的使用者)使用此繼承體系時,它處理的是一個抽象層次的對象集合(即基類)。雖然這個對象集合的成員實質上可能是各種派生類對象,但在處理這個對象集合中的對象時,它用的是抽象層次的操作。并不區分這些操作在派生類是否已經做了修改,也是說使用者并不區分哪些是虛函數哪些是非虛函數,也就是說使用者不理會它使用的是繼承體系中的哪個層次的類對象。這是因為,當運行時實際執行到各操作時,運行時系統能夠識別哪些操作需要用到“動態綁定”,從而找到應該使用哪個類的函數。
因此,對繼承體系的使用者而言,此繼承體系是“透明的”。使用者不必關心繼承體系里邊的繼承關系是如何的錯綜復雜,對它而言它所處理的是一致的對象,它只關心自己的業務邏輯。只要繼承體系提供的接口沒有變化,無論繼承體系內部類的層次如何變更,使用者都不需要做任何改變,這使得程序可以模塊化,使用者就是一個程序模塊,這也意味著可擴展性(繼承體系可以在需要的時候添加類)、可維護性(繼承體系可以修改內部的實現)、以及代碼的可讀性(結構更清晰了)得到提高。(代碼舉例見后文)
虛函數帶來的開銷:
?、??空間:每個支持虛擬函數的類,都有一個虛擬函數表,這個虛擬函數表的大小跟該類擁有的虛擬函數的多少成正比,虛擬函數表屬于類所有,無論有多少個對象,都只有一個虛擬函數表。
?、?空間:通過支持虛擬函數的類生成的每一個對象都有一個指向該類虛擬函數表的指針。有多少個對象,就有多少個虛擬函數表指針。
③ 時間:支持虛擬函數的類的每一個對象,在構造時,都會初始化虛擬函數表指針,使其指向虛擬函數表。
④ 時間:當通過指針或引用調用虛擬函數時,跟普通函數調用相比,會多一個根據虛擬函數指針找到虛擬函數表的操作。
⑤ 可能無法使用內聯函數。因為內聯函數是在“編譯期”,編譯期將調用內聯函數的地方用內聯函數體的代碼代替(內聯展開),但是虛擬函數本質上是“運行期”行為。在“編譯期”,編譯器無法確定要動態綁定的虛函數會綁定到那個函數上,所以無法內聯展開。不過,如果在編譯時能夠確定調用哪個虛函數,那還是可以內聯的,只是,這樣它就失去了作為虛擬函數的功能。
據書上分析,采用虛擬函數跟不采用虛函數相比帶來的負面影響是:虛函數表的空間開銷和無法使用內聯函數。虛函數表占的空間較小,可以忽略,所以主要缺點是無法使用內聯函數。但是不采用虛函數就使得代碼可擴展性和可維護性大大降低,而面向對象編程的一個重要目的就是增加程序的可擴展性和可維護性,即當程序的業務邏輯發生改變時,對原程序的修改非常方便。
因此在性能和其他方面特性的選擇方面,需要開發人員根據實際情況進行權衡和取舍。當然在權衡之前,需要通過性能檢測確認性能的瓶頸是由于虛擬函數沒有利用到內聯函數的優勢這一缺陷引起;否則可以不必考慮虛擬函數的影響。
4、臨時對象
這里所說的臨時對象是未出現在源碼中、不由程序員定義的,從棧中產生的未命名的對象,程序員可能沒有注意到它們的存在,它們是由編譯器根據需要產生、銷毀的。
書上的分析都是以編譯器未進行優化為基礎分析的,即分析的是VS編譯器的debug模式,VS編譯器的release模式做了很多的優化,臨時對象已經減少了很多。
里邊的一條建議確實可以減少臨時對象,release模式下也有用:對于非內建類型的對象,盡量將對象的創建延遲到已經確切知道其有效狀態時。
例如:(1)ClassA? a;
????????????? a= f();//f()內部定義局部對象b,返回該局部變量。
?????? ?(2)ClassA a = f();
上邊的兩種代碼雖然功能一樣,但是在release模式下代碼(1)會多一次構造函數操作和一次operator=操作。分析如下(代碼見后文)。
Release模式下:
(1)代碼分析:①構造對象a;②構造f()內的“局部對象”b,release模式下b的地址空間是在外部分配的,所以再f()函數結束后,b對象仍然有效;③調用operator=操作符,把b對象賦值給a對象。
(2)代碼分析:①構造f()內的“局部對象”b,release模式下b的地址空間是在外部分配的,就是a對象的空間,所以返回后什么也不需要做。
operator+=跟operator+的比較。operator+=不需要產生臨時對象,operator+往往要產生臨時對象。所以盡量使用operator+=。
盡量使用++obj,而盡量不使用obj++。因為obj++會產生臨時對象用于返回++之前的值。
臨時對象的生命周期:從創建開始,到包含創建它的最長語句執行完畢。但有一個例外:當用一個臨時對象來初始化一個常量引用時,該臨時對象的生命會持續到與綁定到其上的常量引用銷毀時。
5、內聯函數
?????? 內聯函數與非內聯函數的空間和時間比較。假設調用一個函數之前的準備工作和之后的善后工作的指令所需空間大小為SS,執行這些代碼所需時間為TS。
(1)空間。如果一個函數的函數體代碼大小為AS,在程序中被調用N次,不采用內聯的情況下,空間開銷為:SS*N+AS。采用內聯:AS*N。因為N一般很大,所以它們之間的比較就是SS跟AS的比較,得出的結論是:如果SS小于AS,不采用內聯,空間開銷更少。如果AS小于SS,則采用內聯,空間開銷更少。
(2)?時間。內聯之后每次調用不再需要做函數調用的準備和善后工作;內聯之后編譯器獲得更多的代碼信息,可以進行更好的代碼優化;內聯之后可以降低代碼“缺頁”的幾率。不過,如果內聯的函數非常大的話,會使得存放代碼的頁面增多,“缺頁”也會相應增加,速度反而下降,所以很大的函數不適合內聯。
內聯函數其他方面的負面影響:
(1)在一個大的工程中,某個內聯函數被多次使用,如果修改了內聯函數,那么就要把用到它的所有編譯單元都重新編譯,可能會花費大量的時間。
(2)如果某開發小組利用了第三方提供的程序庫,使用了第三方的內聯函數,那么當第三方更新內聯函數的實現時,開發小組要使用新的內聯函數版本,就必須重新編譯,而如果此時開發小組的程序已經發布了,那么要重新編譯代價是很高的。而如果不是采用內聯函數,那么就可以不必重新編譯即可利用新版本(可能是通過更新包含該函數的庫實現的)。
不可以使用內聯函數的情況:
(1)遞歸調用。原因1可能不知道會遞歸多少次,所以無法內聯;原因2遞歸函數展開之后可能非常龐大,內聯不合適。
(2)虛函數。原因在前邊已經說過。但也存在例外,通過對象調用函數,這種調用在編譯時可以確定調用哪個函數,所以可以內聯。不過,此時虛函數已經失去了它本來的意義:“通過基類指針或引用調用,到真正運行時才決定調用哪個版本”。
(3)程序入口main()函數肯定不會被內聯。
最后說明:inline關鍵字僅僅是給編譯器一個建議,實際上編譯器會不會內聯完全是它自己做決定。有的函數掉用即便不加上inline關鍵字,編譯器也會根據需要給內聯了。
??
2010-8-22
cs_wuyg@126.com
?
?附測試代碼:
1、構造函數是如何執行的.cpp
//構造函數是如何執行的.cpp //2010.8.21 //coder:cs_wuyg@126.com //參考:《C++應用程序性能優化》2.1節 /* 測試說明:通過輸出結果可以發現,構造函數執行初始化的順序:生成父類實例-->生成子類實例;執行初始化列表-->執行構造函數函數體;按照定義的順序初始化,跟初始化列表順序無關。 析構函數順序:析構子類實例-->析構父類實例。 */ //VS2008 #include <iostream> using namespace std; // class BaseA {public:BaseA(int a = 10) : a(a){cout << "Base::BaseA(int a)" << "\t" << a << endl;}virtual ~BaseA(){cout << "BaseA::~BaseA()" << endl;}private:int a; };class BaseAA : public BaseA {public:BaseAA(int b = 10) : BaseA(b),b(b){cout << "BaseAA::baseAA(int b)" << "\t" << b << endl;}~BaseAA(){cout << "BaseAA::~BaseAA()" << endl;}private:int b; }; // class test {public:test(int t = 10) : t(t){cout << "test::test()" << "\t" << t << endl;}~test(){cout << "test::~test()" << endl;}private:int t; }; // class Derived : public BaseAA {public:Derived(int c = 10) : c(c), BaseAA(c), t2(c)//初始化列表中顯式的初始化了父類BaseAA,去掉BaseAA(c)后會發現輸出的值有變化{cout << "Derived::Derived(int c)" << "\t" << c << endl;}~Derived(){cout << "Derived::~Derived()" << endl;}private:int c;test t1;test t2;test t3; }; // int main() {BaseAA *p = new Derived(30);cout << "----------------------------------------" << endl;delete p;system("pause");return 0; } /* Base::BaseA(int a) 30BaseAA::baseAA(int b) 30 test::test() 10 test::test() 30 test::test() 10 Derived::Derived(int c) 30 ---------------------------------------- Derived::~Derived() test::~test() test::~test() test::~test() BaseAA::~BaseAA() BaseA::~BaseA() 請按任意鍵繼續. . . */2、使用者跟繼承體系簡單舉例.cpp
//使用者跟繼承體系簡單舉例.cpp //2010.8.21 //coder:cs_wuyg@126.com //VS2005/2008 #include <iostream> using namespace std; / /*繼承體系*/ class Base {public:void common(){cout << "common work" << endl;}virtual void special() = 0; };class DerivedA : public Base {public:void special(){cout << "Derived A special work" << endl;} };class DerivedB : public Base {public:void special(){cout << "Derived B special work" << endl;} }; / /*使用者*/ class user {public:void usering(Base *p){p->common();//公有操作p->special();//專有操作} }; / int main() {DerivedA aobj;DerivedB bobj;user userobj;userobj.usering(&aobj);userobj.usering(&bobj);system("pause");return 0; } /* common work Derived A special work common work Derived B special work 請按任意鍵繼續. . . */3、臨時對象release模式下臨時對象產生的測試.cpp
//臨時對象release模式下臨時對象產生的測試.cpp //2010.8.21 //coder:cs_wuyg@126.com //VS2005/2008編譯器, release模式 #include <iostream> using namespace std; // class Base {public:Base(){cout << "constructor" << endl;}Base(const Base& temp){cout << "copy constructor" << endl;}Base&operator=(const Base& temp){cout << "operator=" << endl;return *this;} }; // Base fun() {cout << "fun()" << endl;Base a;return a; } // int main() {//測試1cout << "----------測試1:" << endl;Base aobj = fun();//測試2cout << endl << "----------測試2:" << endl;Base aaobj;aaobj = fun();system("pause");return 0;} /* 測試結果1 ----------測試1: fun() constructor----------測試2: constructor fun() constructor operator= */ /* 通過測試結果可以發現:“對于非內建類型的對象,盡量將對象延遲到已經確切知道其有效狀態時。”這句話的正確性。 */4、臨時對象的生命周期.cpp
//臨時對象的生命周期.cpp //2010.8.21 //coder:cs_wuyg@126.com //參考《C++應用程序性能優化》2.3節 //VS2005/2008編譯器 #include <iostream> #include <string> using namespace std;int main() {string stra = "frist";string strb = "second";const char* str;/*測試1*/cout << "----------測試1:" << endl;if (strlen(str = (stra + strb).c_str()) > 5){/*輸出失敗,因為此時(stra + strb).c_str()這個臨時變量已經失效了*/cout << str << endl;}/*測試2*/cout << "----------測試2:" << endl;const string& strc = stra + strb;cout << strc << endl;return 0; } /* 測試結果: ----------測試1:----------測試2: fristsecond */ /* 測試表明: 臨時對象的生命周期:從創建開始,到包含創建它的最長語句執行完畢。 但有一個例外:當用一個臨時對象來初始化一個常量引用時,該臨時對象的生命會持續到與綁定到其上的常量引用銷毀時。 */轉載于:https://www.cnblogs.com/cswuyg/archive/2010/08/22/1805840.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的《C++应用程序性能优化::第二章C++语言特性的性能分析》学习和理解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Oops! the requested
- 下一篇: s3c2440移植MQTT