More Effective C++ 條款18 (轉)

worldblog發表於2008-01-28
More Effective C++ 條款18 (轉)[@more@]

 條款18:分期攤還期望的計算:namespace prefix = o ns = "urn:schemas--com::office" />

在條款17中,我極力稱讚懶惰的優點,儘可能地拖延時間,並且我解釋說懶惰如何提高的執行。在這個條款裡我將採用一種不同的態度。這裡將不存在懶惰。我鼓勵你讓程式做的事情比被要求的還要多,透過這種方式來提高的。這個條款的核心就是over-eager evaluation(過度熱情計演算法):在要求你做某些事情以前就完成它們。例如下面這個模板類,用來表示放有大量數字型資料的一個集合:

template

class DataCollection {

public:

  NumericalType min() const;

  NumericalType max() const;

  NumericalType avg() const;

  ...

};

假設min,max和avg分別返回現在這個集合的最小值,最大值和平均值,有三種方法實現這三種函式。使用eager evaluation(熱情計演算法),當min,max和avg函式被時,我們檢測集合內所有的數值,然後返回一個合適的值。使用lazy evaluation(懶惰計演算法),只有確實需要函式的返回值時我們才要求函式返回能用來確定準確數值的資料結構。使用 over-eager evaluation(過度熱情計演算法),我們隨時跟蹤目前集合的最小值,最大值和平均值,這樣當min,max或avg被呼叫時,我們可以不用計算就立刻返回正確的數值。如果頻繁呼叫min,max和avg,我們把跟蹤集合最小值、最大值和平均值的開銷分攤到所有這些函式的呼叫上,每次函式呼叫所分攤的開銷比eager evaluation或lazy evaluation要小。

隱藏在over-eager evaluation後面的思想是如果你認為一個計算需要頻繁進行。你就可以設計一個資料結構高效地處理這些計算需求,這樣可以降低每次計算需求的開銷。

採用over-eager最簡單的方法就是caching(快取)那些已經被計算出來而以後還有可能需要的值。例如你編寫了一個程式,用來提供有關僱員的資訊,這些資訊中的經常被需要的部分是僱員的辦公隔間號碼。而假設僱員資訊在裡,但是對於大多數應用程式來說,僱員隔間號都是不相關的,所以資料庫不對查抄它們進行。為了避免你的程式給資料庫造成沉重的負擔,可以編寫一個函式findCubicleNumber,用來cache查詢的資料。以後需要已經被獲取的隔間號時,可以在cache裡找到,而不用向資料庫查詢。

以下是實現findCubicleNumber的一種方法:它使用了標準模板庫(STL)裡的map(有關STL參見條款35)。

int findCubicleNumber(const string& employeeName)

{

  // 定義靜態map,儲存 (employee name, cubicle number)

  // pairs. 這個 map 是local cache。

  typedef map CubicleMap;

  static CubicleMap cubes;

 

  // try to find an entry for employeeName in the cache;

  // the STL iterator "it" will then point to the found

  // entry, if there is one (see for details)

  CubicleMap::iterator it = cubes.find(employeeName);

 

  // "it"'s value will be cubes.end() if no entry was

  // found (this is standard STL behavior). If this is

  // the case, consult the database for the cubicle

  // number, then add it to the cache

  if (it == cubes.end()) {

  int cubicle =

  the result of looking up employeeName's cubicle

  number in the database;

 

  cubes[employeeName] = cubicle;  // add the pair

   // (employeeName, cubicle)

  // to the cache

  return cubicle;

  }

  else {

  // "it" points to the correct cache entry, which is a

  // (employee name, cubicle number) pair. We want only

  // the second component of this pair, and the member

  // "second" will give it to us

  return (*it).second;

  }

}

不要陷入STL程式碼的實現細節裡(你讀完條款35以後,你會比較清楚)。應該把注意力放在這個函式蘊含的方法上。這個方法是使用local cache,用開銷相對不大的中查詢來替代開銷較大的資料庫查詢。假如隔間號被不止一次地頻繁需要,在findCubicleNumber內使用cache會減少返回隔間號的平均開銷。

(上述程式碼裡有一個細節需要解釋一下,最後一個語句返回的是(*it).second,而不是常用的it->second。為什麼?答案是這是為了遵守STL的規則。簡單地說,iterator是一個物件,不是指標,所以不能保證”->”被正確應用到它上面。不過STL要求”.”和”*”在iterator上是合法的,所以(*it).second在語法上雖然比較繁瑣,但是保證能執行。)

catching是一種分攤期望的計算開銷的方法。Prefetching(預提取)是另一種方法。你可以把prefech想象成購買大批商品而獲得的折扣。例如控制器從磁碟讀取資料時,它們會讀取一整塊或整個扇區的資料,即使程式僅需要一小塊資料。這是因為一次讀取一大塊資料比在不同時間讀取兩個或三個小塊資料要快。而且顯示如果需要一個地方的資料,則很可能也需要它旁邊的資料。這是位置相關現象,正因為這種現象,設計者才有理由為指令和資料使用磁碟cache和記憶體cache,還有使用指令prefetch。

你說你不關心象磁碟控制器或 cache這樣低階的東西。沒有問題。prefetch在高階應用裡也有優點。例如你為dynamic陣列實現一個模板,dynamic就是開始時具有一定的尺寸,以後可以自動擴充套件的陣列,所以所有非負的都是合法的:

template  // dynamic陣列

class DynArray { ... };  // 模板

 

DynArray a;  // 在這時, 只有 a[0]

  // 是合法的陣列元素

 

 

a[22] = 3.5;   // a 自動擴充套件

  //: 現在索引0-22

  // 是合法的

 

a[32] = 0;  // 有自行擴充套件;

  // 現在 a[0]-a[32]是合法的

一個DynArray物件如何在需要時自行擴充套件呢?一種直接的方法是分配所需的額外的記憶體。就象這樣:

template

T& DynArray::operator[](int index)

{

  if (index < 0) {

  throw an exception;  // 負數索引仍不

  }  // 合法

 

  if (index >當前最大的索引值) {

  呼叫new分配足夠的額外記憶體,以使得

  索引合法;

  }

 

  返回index位置上的陣列元素;

}

每次需要增加陣列長度時,這種方法都要呼叫new,但是呼叫new會觸發operator new(參見條款8),operator new (和operator delete)的呼叫通常開銷很大。因為它們將導致底層的呼叫,系統呼叫的速度一般比程式內函式呼叫的速度慢。因此我們應該儘量少使用系統呼叫。

使用Over-eager evaluation方法,其原因我們現在必須增加陣列的尺寸以容納索引i,那麼根據位置相關性原則我們可能還會增加陣列尺寸以在未來容納比i 大的其它索引。為了避免為擴充套件而進行第二次(預料中的)記憶體分配,我們現在增加DynArray的尺寸比能使i 合法的尺寸要大,我們希望未來的擴充套件將被包含在我們提供的範圍內。例如我們可以這樣編寫DynArray::operator[]

template

T& DynArray::operator[](int index)

{

  if (index < 0) throw an exception;

 

  if (index > 當前最大的索引值) {

  int diff = index – 當前最大的索引值;

 

  呼叫new分配足夠的額外記憶體,使得

  index+diff合法;

  }

 

  返回index位置上的陣列元素;

}

這個函式每次分配的記憶體是陣列擴充套件所需記憶體的兩倍。如果我們再來看一下前面遇到的那種情況,就會注意到DynArray只分配了一次額外記憶體,即使它的邏輯尺寸被擴充套件了兩次:

 

DynArray a;  // 僅僅a[0]是合法的

 

a[22] = 3.5;  // 呼叫new擴充套件

  // a的儲存空間到索引44

  // a的邏輯尺寸

  // 變為23

 

a[32] = 0;   // a的邏輯尺寸

  // 被改變,允許使用a[32],

  // 但是沒有呼叫new

如果再次需要擴充套件a,只要提供的新索引不大於44,擴充套件的開銷就不大。

貫穿本條款的是一個常見的主題,更快的速度經常會消耗更多的記憶體。跟蹤執行時的最小值、最大值和平均值,這需要額外的空間,但是能節省時間。Cache運算結果需要更多的記憶體,但是一旦需要被cache的結果時就能減少需要重新生成的時間。Prefetch需要空間放置被prefetch的東西,但是它減少了訪問它們所需的時間。自從有了就有這樣的描述:你能以空間換時間。(然而不總是這樣,使用大型物件意味著不適合虛擬記憶體或cache 頁。在一些罕見的情況下,建立大物件會降低軟體的效能,因為分頁操作的增加(詳見作業系統中記憶體管理  譯者注),cache命中率降低,或者兩者都同時發生。如何發現你正遭遇這樣的問題呢?你必須profile, profile, profile(參見條款16)。

在本條款中我提出的建議,即透過over-eager方法分攤預期計算的開銷,例如caching和prefething,這並不與我在條款17中提出的有關lazy evaluation的建議相矛盾。當你必須支援某些操作而不總需要其結果時,可以使用lazy evaluation用以提高程式執行效率。當你必須支援某些操作而其結果幾乎總是被需要或被不止一次地需要時,可以使用over-eager用以提高程式執行效率。它們對效能的巨大提高證明在這方面花些精力是值得的。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-998640/,如需轉載,請註明出處,否則將追究法律責任。

相關文章