1. 程式人生 > >C++:編寫高效率程式碼

C++:編寫高效率程式碼

概述:

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';

(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)分期攤還期望的計算

待寫