More Effective C++ 18:分期攤還期望的計算

這個條款的核心是over-eager evaluation(過度熱情計算法):在要求你做某些事情以前就完成它們.
考慮下面的類,用來表示放有大量數字型數據的一個集合:

template<class NumericalType> 
class DataCollection 
{ 
public: 
	NumericalType min() const; 
	NumericalType max() const; 
	NumericalType avg() const; 
	... 
};

假設 min,maxavg 函數分別返回現在這個集合的最小值,最大值和平均值,有三種方法實現這三種函數。
使用 over-eager evaluation(過度熱情計算法),我們隨時跟蹤目前集合的最小值,最大值和平均值,這樣當 min,maxavg 被調用時,我們可以不用計算就立刻返回正確的數值。

如果頻繁調用 min,maxavg,我們把跟蹤集合最小值、最大值和平均值的開銷分攤到所有這些函數的調用上,每次函數調用所分攤的開銷比正常計算和懶惰計算要小。

這個方法的思想是:如果你認爲一個計算需要頻繁進行,你就可以設計一個數據結構高效地處理這些計算需求,這樣可以降低每次計算需求時的開銷.

採用過度熱情最簡單的方法就是緩存那些已經被計算出來而以後還有可能需要的值。
比如:你編寫了一個程序,用來提供有關僱員的信息,這些信息中的經常被需要的部分是僱員的辦公隔間號碼。而假設僱員信息存儲在數據庫裏,但是對於大多數應用程序
來說,僱員隔間號都是不相關的,所以數據庫不對它們進行優化。爲了避免你的程序給數據庫造成沉重的負擔,可以編寫一個函數 findCubicleNumber,用來緩存查找到的數據。以後需要已經被獲取的隔間號時,可以在 cache 裏找到,而不用向數據庫查詢:

int findCubicleNumber(const string& employeeName)
{
	using CubicleMap = map<string,int>;
	static CubicleMap cubes;
	CubicleMap::iterator it = cubes.find(employeeName);
	if(it == cubes.end())
	{
		int cubicle = the result of looking up employeeName's cubicle number in the database;
		cubes[employeeName] = cubicle ;
		return cubicle;
	}
	else
	{
		return (*it).second;
	}
}

這個方法是使用 local cache,用開銷相對不大的內存中查詢來替代開銷較大的數據庫查詢。假如隔間號被不止一次地頻繁需要,在findCubicleNumber 內使用緩存會減少返回隔間號的平均開銷。

緩存是一種分攤期望的的計算開銷的方法。預提取是另一種方法。你可以把預提取想象成購買大批商品而獲得的折扣。

例如磁盤控制器從磁盤讀取數據時,它們會讀取一整塊或整個扇區的數據,即使程序僅需要一小塊數據。這是因爲一次讀取一大塊數據比在不同時間讀取兩個或三個小塊數據要快。

而且經驗顯示如果需要一個地方的數據,則很可能也需要它旁邊的數據。這是位置相關現象。

預提取在高端應用裏也有優點。例如你爲 dynamic 數組實現一個模板,dynamic就是開始時具有一定的尺寸,以後可以自動擴展的數組,所以所有非負的索引都是合法的:

template<class T> // dynamic 數組 
class DynArray { ... };
DynArray<double> a;//在這時, 只有 a[0]是合法的數組元素
a[22] = 3.5;//a 自動擴展, 現在索引 0-22是合法的
a[32] = 0;//又自行擴展,現在 a[0]-a[32]是合法的

一個 DynArray 對象如何在需要時自行擴展呢?一種直接的方法是分配所需的額外的內存。就像這樣:

template<class T> 
T& DynArray<T>::operator[](int index) 
{ 
	if (index < 0) 
	{ 
		throw an exception; // 負數索引仍不合法 
	} 
	if (index >當前最大的索引值) 
	{ 
		調用 new 分配足夠的額外內存,以使得 
		索引合法; 
	} 
	返回 index 位置上的數組元素; 
}

每次需要增加數組長度時,這種方法都要調用 new,但是調用 new 會觸發 operator newoperator new的調用通常開銷很大。因爲它們將導致底層操作系統的調用,系統調用的速度一般比進程內函數調用的速度慢。因此我們應該儘量少使用系統調用。

使用Over-eager evaluation方法,我們現在必須增加數組的尺寸以容納索引 i,那麼根據位置相關性原則我們可能還會增加數組尺寸以在未來容納比 i 大的其它索引。爲了避免爲擴展而進行第二次內存分配,我們現在增加 DynArray 的尺寸比能使 i 合法的尺寸要大,我們希望未來的擴展將被包含在我們提供的範圍內。例如我們可以這樣編寫:

template<class T> 
T& DynArray<T>::operator[](int index) 
{ 
	if (index < 0) 
	{ 
		throw an exception; // 負數索引仍不合法 
	} 
	if (index >當前最大的索引值) 
	{ 
		int diff = index – 當前最大的索引值;
		調用 new 分配足夠的額外內存,使得 index+diff 合法;
	} 
	返回 index 位置上的數組元素; 
}

這個函數每次分配的內存是數組擴展所需內存的兩倍。如果我們再來看一下前面遇到的那種情況,就會注意到 DynArray 分配了一次額外內存,即使它的邏輯尺寸被擴展了兩次:

DynArray<double> a;//在這時, 只有 a[0]是合法的數組元素
a[22] = 3.5;//調用 new 擴展 a 的存儲空間到索引 44
a[32] = 0;//允許使用 a[32],但是沒有調用 new

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

更快的速度經常會消耗更多的內存。跟蹤運行時的最
小值、最大值和平均值,這需要額外的空間,但是能節省時間。緩存運算結果需要更多的內存,但是一旦需要被緩存的結果時就能減少需要重新生成的時間。
預提取需要空間放置被預提取的東西,但是它減少了訪問它們所需的時間。也就是空間換時間。

總結

當你必須支持某些操作而不總需要其結果時,懶惰計算是在這種時候用以提高程序效率的技術,當你必須支持某些操作而其結果幾乎總是被需要或被不止一次地需要時,over-eager 是在這種時候使用的用以提高程序效率的一種技術。它們所產生的巨大的性能提高證明在這方面花些精力是值得的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章