概述:
C++相比其他高級語言效率高的多,也有許多程序使用C++作為內核以提高程序的性能瓶頸,一個太大太慢的程序他們的優點無論有多么引人注目都不會為人們所接受,盡管有一些程序的確是為了復雜的運算才占用更多的時間和空間,但是更多的程序只能歸咎於糟糕的設計和馬虎的編程。想用C++寫出高效的代碼之前,必須認識到C++本身絕對與你所遇到的任何性能上的問題無關。如果想寫出一個高效的C++程序,你必須首先寫出一個高效的算法。太多的開發人員都忽略了這樣一個簡單的道理。例如:循環能夠被手工展開,移位操作能夠代替乘法,傳遞臨時變量時候使用std::move,異常拋出時候對效率的影響,不斷的產生臨時變量調用構造函數析構函數。但是如果你使用的高層算法內在效率很低, 這些微調就不會有任何作用。
80-20准則:
80-20准則說的是大約20%的代碼使用了80%的程序資源 ;大約20%的代碼耗用了大約80%的運行時間;大約20%的代碼使用了80%的內存;大約20%的代碼執行80%的磁盤訪問,大約80%的的維護投入大約20%的代碼上;通過無數台機器、操作系統和應用程序上的實驗這條准則已經被再三驗證過。
當程序員力爭最大化的提升軟件性能時,80-20准則技能簡化了你的工作又使你的工作變得復雜。一方面80-20准則標識大多是時間你能夠編寫性能一般的代碼,因為80%的時間里這些代碼的效率不會影響到整個系統的性能,這回減少一些你的工作壓力。而另一方面這條准則也表示如果你的軟件出現了性能問題,你將面臨一個困難的工作,因為你不進必須找到導致問題的那一小塊代碼的位置,還必須尋找到方法提高它們的性能。這些任務中最困難的一般是找到系統瓶頸,基本有兩個不同的方法來尋找:大多數人用的方法和正確的方法。
大多數人尋找瓶頸的方法就是猜。通過經驗,直覺。一個又一個程序員一本正經的宣稱程序性能的瓶頸已經被找到,因為網絡的延遲,不正確的內存分配,編譯器沒有進行足夠的優化或者一些拒絕在關鍵循環里使用匯編語句。這些評估和預言都是錯誤的。大多數程序員在他們的性能特征上的直覺都是錯誤的,因為程序性能特征往往不能靠直覺來確定。覺過為提高程序各部分的效率而傾注了大量的精力,但是對程序的整體性能沒有顯著的影響。例如在程序里使用能夠最小化計算量的奇特算法和數據結構,但是如果程序的性能主要限制在IO上,那么就絲毫起不到作用。采用IO性能搶勁的程序庫代替編譯器本身附加的程序庫,如果程序的性能瓶頸主要在CPU上,這種方法也不起什么作用。
正確的方法是用profiler程序識別出程序的20%部分。不是所有工作都讓profiler去做。你想讓它去直接地測量你感興趣的資源。例如如果程序太慢,你想讓profiler告訴你程序的各個部分都耗費了多少時間。然后你關注局部效率能夠被極大提高的地方,這也會將很大地提高整體的效率。
使用懶惰計算法:
(1)引用計數:
1 class String {...}; 2 3 String s1 = "HelloWorld"; 4 String s2 = s1; // 會調用String的構造拷貝函數
通常String的拷貝構造函數讓s2被s1初始化以后,s1和s2都有自己對字符串的一份拷貝,這種拷貝會引起較大的開銷(這屬於熱情計算)。
懶惰就是少做工作,不應該讓給s2一個s1的拷貝,而是讓s2與s1共享一個值。我們只需要記錄一下便知道是誰在共享什么,就能夠剩掉new和strcpy的開銷。這種方法只有在修改某個String時才會有差別,會造成所有共享String的變量值都被修改,為了解決這樣的問題,可以在被修改時進行拷貝值,然后再新建一份拷貝的共享。
(2)區別對待讀取和寫入:
1 String s= "HelloWorld"; 2 // ... 4 std::cout << s[3]; 5 s[3] = 'x';
有人可能想了,我們可以利用常量性對operator[]重載,使得區分讀寫動作,使用代碼來呈現的話,大致是這樣的
1 class String { 2 public: 3 const char &operator[](int idx) const; // 針對讀操作 4 char &operator[](int idx); // 針對寫操作 5 };
但是在現實中,這是沒有用的。編譯器在const和no-const之間的選擇,只以“調用該函數的對象是否是const為基准”,並不考慮它們在什么情況下被調用。因此
1 String s1, s2; 2 // ... 3 std::cout << s1[5]; // 調用no-const operator[],因為s1不是const 4 5 s2[5] = 'x'; // 也調用no-const operator[],因為s2不是const 6 s1[3] = s2[8]; // 左右都是調用no-const operator[]
事實證明,重載operator[]不能區分讀寫操作
我們的想法基於一個事實:雖然或許不知道operator[]是在左值還是右值情境下被調用,我們還是可以區分讀寫操作-只要我們將動作延緩,直至知道operator[]的返回結果將是如何被使用的。我們需要知道的,就是如何延緩我們的決定(決定對象是被讀或者寫),直到operator[]返回。
Proxy class(代理類)可以讓我們實現想要的效果,示例代碼如下:
1 class CharProxy { 2 public: 3 CharProxy(String &str, int idx); 4 CharProxy &operator=(const CharProxy &rhs); //左值運用 5 CharProxy &operator](char c); //左值運用 6 operator char() const; //右值運用 7 private: 8 String &str_; 9 int idx_; 10 }; 11 12 // 我們將String的operator[]重載以后返回這個代理類 13 // 就可以有效的實現區分讀取和寫操作 14 15 String s1, s2; 16 std::cout << s1[5]; //調用CharProxy::operator char() const; 17 s2[5] = 'x'; //調用CharProxy &operator](char c);
使用Proxy class很適合區分operator[]是左值還是右值運用,但是這項技術並非沒有缺點,我們希望Proxy class能夠無間隙地取代她所代表的對象,但是這樣的想法很難實現,因為除了賦值操作,對象極有可能在其他情景被使用,而在那種情況下,Proxy class的表現與真實對象可能會有出入。
1 // 考慮一下代碼 2 String s1 = "Hello"; 3 char *p = &s1[1]; //錯誤 4 // 或許我們可以重載&操作符 5 // 可以思考一下還能如何解決
(3)懶惰提取:
假如程序使用了一些包含許多大字段的大型對象。這些對象的生存期超越了程序運行期,所以他們必須被儲存到數據庫。每一個對都有一個唯一的標識符,用來從數據庫中重新獲得對象
1 class LargeObject { 2 public: 3 LargeObject(ObjectID id); // 從磁盤中恢復對象 4 const string &field1(); // 字段的值 5 // ... 6 }; 7 8 // 現在考慮一下從磁盤中回復LargeObject的開銷 9 void restoreAndProcessObject(ObjectID id) { 10 LargeObject obj(id); // 恢復對象 11 } 12 13 // 因為LargeObject對象實例很大,為這樣的對象獲取所有數據,數據庫的操作開銷非常大,特別是遠程數據庫中獲取內存,而在這種情況下不需要去讀所有數據,例如 14 void restoreAndProcessObject(ObjectID id) { 15 LargeObject obj(id); 16 if(obj.field1() == "x") { 17 std::cout << "xxxx" << std::endl; 18 } 19 } 20 // 這里只需要filed1的值,為其獲取其他字段而付出的努力都是白費的 21 // 當LargeObject對象被建立時,不需要從磁盤讀取所有數據,這樣懶惰法解決了這個問題,不過對象建立時候僅僅是一個殼,而當需要某個數據時,這個數據才被從數據庫中取回。
(4)懶惰表達式計算:
1 // 考慮這樣的代碼 2 template<class T> 3 class Matrix { ... }; 4 Matrix<int> m1(1000, 1000); // 一個1000*1000的矩陣 5 Matrix<int> m2(1000, 1000); 6 // ... 7 Matrix<int> m3 = m1 + m2; 8 // operator+函數計算m1與m2的和。這個計算量相當大 9 // 懶惰表達式計算方法說這樣工作太多,所以還是不要去做了。而是應該建立一個數據結構來標識m3的值是m1與m2的和,在用一個enum標識他們之間是加法操作。很明顯,建立這個數據結構比m1與m2相加要快許多
(5)分期攤還期望的計算
待寫