《More Effective C++》總結筆記(三)

效率

條款16:謹記80-20法則

  • 80-20法則說:一個程序80%的資源用於20%的代碼身上。是的。80%的執行時間花在大約20%的代碼身上,80%的內存被大約20%的代碼使用,80%的磁盤訪問動作由20%的代碼執行,80%的維護力氣花在20%的代碼上面。
  • 不必拘泥於法則的數字,其基本重點是:軟件的整體性能幾乎總是由其構成要素(代碼)的一小部分決定。
  • 從某個角度看,80-20法則暗示,大部分時候你所產出的代碼,其性能坦白說是平凡的,因爲80%的時間中,其效率不會影響系統整體性能。或許這不至於對你的自尊心造成太大打擊,但應該多少會降低你的壓力。從另一個角度看,這個法則暗示,如果你的軟件有性能上的問題,你將面臨悲慘的前景,因爲你不只需要找出造成問題的那一小段瓶頸所在,還必須找出辦法來大幅提升其性能。這些工作中,最麻煩的還是找出瓶頸所在。
  • 找性能瓶頸的方法不能靠猜。可行之道是完全根據觀察或實驗來識別出造成你心痛的那20%代碼。而辨識之道就是藉助某個程序分析器。然而並不是任何分析器都足堪大任,它必須可以直接測量你所在意的資源。

條款17:考慮使用lazy evaluation(緩式評估)

  • 一旦你採用lazy evaluation,就是以某種方式撰寫的你classes,使它們延緩運算,直到那些運算結果刻不容緩地被迫切需要爲止。如果其預算結果一直不被需要,運算也就一直不執行。
  • lazy evaluation在許多領域中都可能有用途:可避免非必要的對象複製,可區別operator[]的讀取和寫動作,可避免非必要的數據庫讀取動作,可避免非必要的數值計算動作。

條款18:分期攤還預期的計算成本

  • 此條款背後的哲學可稱爲超急評估(over-eager evaluation):在被要求之前就先把事情做下去。
  • over-eager evaluation背後的觀念是,如果你預期程序常常會用到某個計算,你可以降低每次計算的平均成本,辦法就是設計一份數據結構以便能夠極有效率地處理需求。
  • 其中個最簡單的一個做法就是將“已經計算好而有可能再被需要”的數值保留下來(所謂caching)。另一種做法則是預先取出(prefetching)。prefetching的一個典型例子就是std的vector數組擴張的內存分配策略(每次擴大兩倍)。
  • 條款17和18看似矛盾,實際都體現了計算機中一個古老的思想:用空間換時間。結合起來看,它們是不矛盾的。當你必須支持某些運算而其結果並不總是需要的時候,lazy evaluation可以改善程序效率。當你必須支持某些運算而其結果幾乎總是被需要,或其結果常常被多次需要的時候,over-eager evaluation可以改善程序效率。

條款19:瞭解臨時對象的來源

  • C++真正的所謂的臨時對象時不可見的——不會在你的源代碼中出現。只要你產生一個non-heap object而沒有爲它命名,便誕生了一個臨時對象。此等匿名對象通常發生於兩種情況:一是當隱式類型轉換被施行起來以求函數調用能夠成功;二是當函數返回對象的時候。
  • 任何時候只要你看到一個reference-to-const參數,就極可能會有一個臨時對象被產生出來綁定至該參數上。任何時候只要你看到函數返回一個對象,就會產生臨時對象(並於稍後銷燬)。學些找出這些架構,你對幕後成本(編譯器行爲)的洞察力將會有顯著地提升。

條款20:協助完成“返回值優化(RVO)”

  • 如果函數一定得以by-value方式返回對象,你絕對無法消除之。從效率的眼光來看,你不應該在乎函數返回了一個對象,你應該在乎的是那個對象的成本幾何。你需要做的,是努力找出某種方法以降低被返回對象的成本,而不是想盡辦法消除對象本身。
  • 我們可以用某種特殊寫法來撰寫函數,使它在返回對象時,能夠讓編譯器消除臨時對象的成本。我們的伎倆是:返回所謂的constructor arguments以取代對象。考慮分數(rational numbers)的operator*函數,一個有效率而且正確的做法是:
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
  • C++允許編譯器將臨時對象優化,使它們不存在。於是如果你這樣調用operator*:
Rational a = 10;
Rational b(1,2);
Rational c = a * b;
  • 你的編譯器得以消除“operator*內的臨時對象”及“被operator*返回的臨時對象”。它們可以將return表達式所定義的對象構造於c的內存內。
  • 你可以將此函數聲明爲inline,以消除調用operator*所花費的額外開銷。這就是返回一個對象最有效率的做法。

條款21:利用重載技術(overload)避免隱式類型轉換(implicit type conversions)

  • 考慮以下代碼:
class UPInt { // 這個class用於無限精密的整數
plublic:
    UPInt();
    UPInt(int value);
    ...
};
const UPInt operator+(const UPInt& lhs, const UPInt& rhs);
UPInt upi1, upi2;
...
UPInt upi3 = upi1 + upi2;
upi3 = upi1 + 10;
upi3 = 10 + upi2;
  • 因爲有隱式轉換,所以以上語句都能執行成功。但這樣執行會產生臨時對象,這對執行效率是有影響的。
  • 如果有其他做法可以讓operator+在自變量類型混雜的情況下唄調用成功,那便消除了了類型學轉換的需求。如果我們希望能夠對UPInt和int進行加法,我們需要做的就是將我們的意圖告訴編譯器,做法是聲明數個函數,每個函數有不同的參數表:
const UPInt operator+(const UPInt& lhs, const UPInt& rhs); // 將UPInt和UPInt相加
const UPInt operator+(const UPInt& lhs, const int& rhs); // 將UPInt和int相加
const UPInt operator+(const int& lhs, const UPInt& rhs); // 將int和UPInt相加
  • 但是不要寫出以下函數:
const UPInt operator+(const int& lhs, const int& rhs);
  • 因爲,C++存在很多遊戲規則,其中之一就是:每個“重載操作符”必須獲得至少一個“用戶定製類型”的自變量。int不是用戶定製類型,所以我們不能夠將一個只獲得int自變量的操作符加以重載,這會改變ints的加法意義。

條款22:考慮以操作符複合形式(op=)取代其獨身形式(op)

  • 要確保操作符的複合形式(例如,operator+=)和其獨身形式(例如,operator+)之間的自然關係能夠存在,一個好方法就是以前者爲基礎實現後者:
const Rational operator+(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs) += rhs;
}
  • 一般而言,複合操作符比其對應的獨身版本效率高,因爲獨身版本通常必須返回一個新對象,而我們必須因此負擔一個臨時對象的構造和析構成本。至於複合版本則是直接將結果寫入其左端自變量,所以不需要產生一個臨時對象來放置返回值。
  • 操作符我複合版本比其對應的獨身版本有着更高效率的傾向。身爲一位程序庫設計者,你應該兩者都提供;身爲一位應用軟件開發者,如果性能是重要因素的話,你應該考慮以複合版本操作符取代其獨身版本。

條款23:考慮使用其他程序庫

  • 不同的設計者面對不同的特性會給予不同的優先權。他們的設計各有不同的犧牲。於是,很容易出現“兩個程序庫提供類似機能,卻又相當不同的性能表現”的情況。
  • 所以一旦你找出程序的性能瓶頸(通過分析器),你應該思考是否有可能因爲改用另一個程序庫而移除了那些瓶頸。

條款24:瞭解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本

  • 虛函數的第一個成本:你必須爲每個擁有虛函數的class耗費一個vtbl空間,其大小視虛函數的個數(包括繼承而來的)而定。
  • 虛函數的第二個成本:你必須在每一個擁有虛函數的對象內付出“一個額外指針”的代價。
  • inline意味着在編譯期將調用端的調用動作被調用函數的函數本體取代,而virtual則意味着直到運行期才直到哪個函數被調用。因此虛函數的第三個成本就是:你事實上等於放棄了inlining。
  • 多重繼承往往導致virtual base classes(虛擬基類)的需求。virtual base classes可能導致對象內的隱藏指針增加。
  • RTTI讓我們得以在運行期獲得objects和classes的相關信息,它們本存放在類型爲type_info的對象內。你可以利用typeid操作符取得某個class相應的type_info對象。
  • RTTI的設計理念是:根據class的vtbl來實現。舉個例子,vtbl數組中,索引爲0的條目可能內含一個指針,指向“該vtbl所對應的class”的相應的type_info對象。運用這種實現方法,RTTI的空間成本就只需在每一個class vtbl內增加一個條目,再加上每個class所需的一份type_info對象空間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章