1. Over-eager evaluation("超急评估"):超前进度地做"要求以外"的更多工作,也就是在要求之前就把事情做下去.例如,以下class template用于表现数值数据的大型收集中心:
template<class NumericalType> class DataCollection{ public: NumericalType min()const; NumericalType max()const; NumericalType avg()const; ... }
min,max,avg函数分别返回数据群的最大,最大,平均值,其实现方法有三种:第一种是使用eager evaluation,在min,max,avg被调用时才检查所有数据,然后返回检查结果;第二种是使用lazy evaluation(见条款17),令这些函数返回某些数据结构,直到"这些函数的返回值真正被派上用场"时,才计算它们的值;第三种就是over-eager evaluation,随时记录程序集执行过程中的最小,最大,平均值,一旦min,max,avg常被调用,便可以分期(逐次)摊还"随时记录数据群之最大,最小,平均值"的成本,而每次调用所需付出的(摊还后)成本,将比eager evaluation和lazy evaluation低.
2. Over-eager的背后观念是,如果预期程序常常会用到某个计算,可以降低每次计算的平均成本,方法就是设计一份数据结构以便能够极有效率地处理需求.
其中一种设计方法就是将"已经计算好而有可能再次被需要"的数值保留下来(所谓caching),假设职员信息被储存在一个数据库中,需要写一个程序来提供职员信息,这个程序预计会常常使用员工的房间号码,但是该数据库并没有这对此数据段做优化.为避免重复寻找房间号码而造成数据库的不当压力,可以写一个findCubicNumber函数,将它找到的房间号码记录下来,后续再有号码的查询需求,如果数据已取出,就可以借助缓存(caching)完成任务而不必查询数据库.
下面是findCubicNumber的一种设计方法,使用STL的map容器作为局部缓存:
int findCubicNumber(const string& employeeName){ //map存储(employee,cubicle number)数据对,作为局部缓存(local cache) typedef map<string,int> CubicleMap; static CubicleMap cubes; Cubic::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; }
此策略将昂贵的"数据库查询动作"以相对低廉的"内存内数据结构产找动作"取代,从而降低"返回一个职员的房间号码"的平均成本.
另一种做法是Prefetching(预先取出)..磁盘控制器从磁盘读取数据时,读取的是整个数据块或sectors——即使程序只需要其中少量数据.因为一次读取一大块数据比分成两三次每次读小块数据速度上快得多,而locality of reference现象(被取用的数据有"位置集中"的现象)显示了这种策略的优越性.系统设计者依次现象设计出磁盘缓存(disk caches),指令与数据的内存缓存(memory caches),以及"指令预先取出"(instruction prefetches).
假设要实现一个"动态数组"的template,其大小可随非负索引值实现动态扩张:
template<class T> class DynArray{...}; a[22]=3.5; //a自动扩张,使得有效索引为0~22 a[32]=0; //a自动扩张,使得有效索引为0~32
最直接的策略就是在每次为新加入的索引做动态内存分配行为,像这样:
template<class T> T& DynArray<T>::operator[](int index){ if(index<0){ throw an exception; } if(index>the current maximum index value){ call new to allocate enough additional memory so that index is valid; } return the indexth element of the array; }
此方法是在每次需要增加数组大小时才调用new,但new会调用operator new,而operator new通常代价昂贵,因为它们通常会调用底层操作系统,而"系统调用"往往比"进程
(process)内的函数调用"速度慢.因此应该尽可能不采用系统调用.
使用eager evaluation策略,在为数组新分配内存时,为避免接下来可能的扩张所需要的内存分配成本,可以分配稍大的内存,这其实就是STL中vector等容器所采用的内存池策略(的一部分,内存池策略还包括预先分配raw memory,等到使用时再构造对象).因此DynArray::operator[]可以撰写如下:
template<class T> T& DynArray<T>::operator[](int index){ if(index<0) throw an exception; if(index>the current maximum index value){ int diff=index-the current maximum index value; call new to allocate enough additional memory so that index+diff is valid; } return the indexth element of the array; }
此函数在数组需要扩张时,分配两倍内存.对于此策略实现的operator[]调用之前代码,虽然逻辑上数组大小扩张了两次,但实际上DynArray只需分配一次内存,第二次扩张成本极低.
3. Over-eager evaluation实际上是一种"以空间换时间"的策略:Caching和Fetching都需要额外空间来(待)存放数据以降低访问所需时间.少数情况下,对象变大会降低软件性能:换页(paging)活动可能会增加,缓存击中率(cache hit rate)可能会降低.
over-eager evaluation和条款17的lazy evaluation并不矛盾:当必须支持某些运算但运算结果并不总是需要的时候,lazy evaluation可以改善程序效率;当必须支持某些运算且其结果总是被需要,或常常被需要的时候,over-eager evaluation可以改善程序效率.两者都比直接了当的eager evaluation难实现,但都可以为程序带来巨大的性能提升.