这个条款的核心是over-eager evaluation(过度热情计算法):在要求你做某些事情以前就完成它们.
考虑下面的类,用来表示放有大量数字型数据的一个集合:
template<class NumericalType>
class DataCollection
{
public:
NumericalType min() const;
NumericalType max() const;
NumericalType avg() const;
...
};
假设 min
,max
和 avg
函数分别返回现在这个集合的最小值,最大值和平均值,有三种方法实现这三种函数。
使用 over-eager evaluation
(过度热情计算法),我们随时跟踪目前集合的最小值,最大值和平均值,这样当 min
,max
或 avg
被调用时,我们可以不用计算就立刻返回正确的数值。
如果频繁调用 min
,max
和 avg
,我们把跟踪集合最小值、最大值和平均值的开销分摊到所有这些函数的调用上,每次函数调用所分摊的开销比正常计算和懒惰计算要小。
这个方法的思想是:如果你认为一个计算需要频繁进行,你就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求时的开销.
采用过度热情最简单的方法就是缓存那些已经被计算出来而以后还有可能需要的值。
比如:你编写了一个程序,用来提供有关雇员的信息,这些信息中的经常被需要的部分是雇员的办公隔间号码。而假设雇员信息存储在数据库里,但是对于大多数应用程序
来说,雇员隔间号都是不相关的,所以数据库不对它们进行优化。为了避免你的程序给数据库造成沉重的负担,可以编写一个函数 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 new
,operator 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
是在这种时候使用的用以提高程序效率的一种技术。它们所产生的巨大的性能提高证明在这方面花些精力是值得的。