【more effective c++读书笔记】【第4章】效率(1)

条款16:谨记80-20法则

80-20法则说:一个程序80%的资源用于20%的代码身上。80%的执行时间花在大约20%的代码上;80%的内存被大约20%的代码使用;80%的磁盘访问动作由20%的代码执行;80%的维护力气化在了20%的代码上。软件的整体性能几乎总是由其构成要素的一小部分决定。


条款17:考虑使用lazyevaluation(缓式评估)

lazy evaluation就是以某种方式写类,将它们延缓运算,直到那些运算结果刻不容缓地被迫切需要为止。如果其运算结果一直不被需要,运算也就一直不执行。

lazy evaluation有以下4种应用场合:

1、ReferenceCounting(引用计数)

例子:

class String { ... }; //一个字符串类
String s1 = "Hello";
String s2 = s1; //调用String的拷贝构造函数

上述例子中eager evaluation(急式评估)是String的拷贝构造函数被调用,就为s1做一个副本并放进s2内,而此时s2尚未真正需要实际内容,因为s2尚未被使用。

缓式(lazy)做法是让s2共享s1的值,而不再给予s2一个s1内容副本。这种做法节省了调用new及复制任何东西的高昂成本。数据共享所引起的唯一危机是在其中某个字符串被修改时发生的。应该修改一个String的值,而不是两个都被修改。

上述数据共享的观念便是lazy evaluation:在真正需要之前,不必着急为某物做一个副本,而以拖延战术应付。

2、区分读和写

String s = "Homer's Iliad"; 
cout << s[3];   // 调用 operator[] 读取s[3]
s[3] = 'x';     // 调用 operator[] 写入 s[3]

上述例子中我们希望能够区分读和写,因为读取reference-counted字符串代价很小,而写入则可能需要在写入前对该字符串做出一个副本。运用lazy evaluation和条款30中描述的proxy class,我们可以延缓是读还是写的决定,直到能够确定其答案为止。

3、LazyFetching(缓式取出)

例子:

class LargeObject {                        // 大型的、可持久存在的对象
public:
	LargeObject(ObjectID id);                // 从磁盘中回存对象

	const string& field1() const;            // 字段1的值
	int field2() const;                      // 字段2的值
	double field3() const;                   // ...
	const string& field4() const;
	const string& field5() const;
	...
};

现在考虑从磁盘中恢复LargeObject对象所需的成本:

void restoreAndProcessObject(ObjectID id){
	LargeObject object(id);  // 恢复对象
	...
}

由于LargeObject体积很大,为此类对象获取所有的数据,数据库操作的成本将非常大,特别是如果从远程数据库中获取数据和通过网络发送数据时。某些的情况下,不需要读取所有数据。例如,考虑这样一个程序:

void restoreAndProcessObject(ObjectID id){
	LargeObject object(id);
	if (object.field2() == 0) {
		cout << "Object " << id << ": null field2./n";
	}
}

这里只用到filed2的值,所以为获取其它字段而付出的努力都是浪费。

当LargeObject对象被建立时,不从磁盘上读取所有的数据,这样懒惰法解决了这个问题。不过这时建立的仅是一个对象“壳“,当需要某个数据时,这个数据才被从数据库中取回。这种” demand-paged”对象初始化的实现方法是:

class LargeObject {
public:
	LargeObject(ObjectID id);
	const string& field1() const;
	int field2() const;
	double field3() const;
	const string& field4() const;
	...
private:
	ObjectID oid;
	mutable string *field1Value;             
	mutable int *field2Value;                 
	mutable double *field3Value;
	mutable string *field4Value;
	...
};

LargeObject::LargeObject(ObjectID id)
: oid(id), field1Value(0), field2Value(0), field3Value(0), ...
{}

const string& LargeObject::field1() const{
	if (field1Value == 0) {
		从数据库中为filed 1读取数据,使
			field1Value 指向这个值;
	}
	return *field1Value;
}

上述例子中指针字段声明为mutable表示这样的字段可以再任何成员函数内被修改,甚至是在const成员函数内。

4、LazyExpression Evaluation (表达式缓评估)

例子:

template<class T>
class Matrix { ... }; 

Matrix<int> m1(1000, 1000);  // 一个 1000 * 1000 的矩阵
Matrix<int> m2(1000, 1000); // 同上
...
Matrix<int> m3 = m1 + m2;    // m1+m2

lazy evaluation策略是应该建立一个数据结构来表示m3的值是m1与m2的和,再用一个enum表示它们间是加法操作。建立这个数据结构比m1与m2相加要快许多,也能够节省大量的内存。

考虑程序后面这部分内容,在使用m3之前,程序执行了以下动作:

Matrix<int> m4(1000, 1000);
...     // 赋给m4某些值
m3 = m4 * m1;

现在我们可以忘掉m3是m1与m2的和(因此节省了计算的开销),在这里应该记住m3是m4与m1运算的乘积。不用说,我们不会马上进行这个乘法。

一个更常见的应用领域是当我们仅仅需要计算结果的一部分时。例如假设我们初始化m3的值为m1和m2的和,然后象这样使用m3:

cout << m3[4];   // 打印m3的第四行

很明显,我们不能再懒惰了,应该计算m3的第四行值。但是我们也不需要费太大的劲,没有理由计算m3第四行以外的结果;m3其余的部分仍旧保持未计算的状态直到确实需要它们的值。

总结:上述4个例子显示lazy evaluation在许多领域中都可能有用途:可避免非需要的对象复制,可区别operator[]的读取和写操作,可避免非必要的数据库读取动作,可避免非必要的数值计算动作。


条款18:分期摊还预期的计算成本

1、超急评估(over-eagerevalution):在被要求之前就先把事情做下去。over-eager evaluation背后的观念是,如果你预期程序常常会用到某个计算,你可以设计一份数据结构高效地处理需求,这样可以降低每次计算的平均成本。

2、分期摊还预期的计算成本的做法有以下两种:

a、将“已经计算好而有可能再被需要”的数值保留下来,即所谓的catching。

例子:

如果程序需要不断查询数据库的一个数据项,我们将它的数据保存下来,当再次查询的时候如果直接取出,就可借用高速缓存(catche)完成任务,不必再去查询数据库了。

int findCubicleNumber(const string& employeeName){
	//定义静态map,存储 (employee name, cubicle number)数据对
	//这个map用来作为局部缓存
	typedef map<string, int> CubicleMap;
	static CubicleMap cubes;
	//尝试在cache中针对employeeName找出一笔记录。如果确有一笔
	//it便指向该笔被找到的记录
	CubicleMap::iterator it = cubes.find(employeeName);
	//如果找不到任何吻合记录,那么就针对此房间号码查询数据库
	//然后把它也加进cache之中
	if (it == cubes.end()) {
		int cubicle =
			the result of looking up employeeName's cubicle
			number in the database;
		cubes[employeeName] = cubicle;
		return cubicle;
	}
	else {
		//it指向正确的cache记录,我们只需第二项数据
		return (*it).second;
	}
}

b、prefetching(预先取出)是另一种做法。

例子:

希望实现一个动态数组的template,也就是数组大小一开始为1,然后自动扩张,使所有非负索引值都有效。

template<class T>    //动态数组
class DynArray { ... };                    

DynArray<double> a;  //此时, 只有 a[0]是合法的数组元素
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 >当前最大的索引值) {
		调用new分配足够的额外内存,以使得索引合法;
	}
	返回index位置上的数组元素;
}

上述做法在每次需要增加数组长度时调用new,但是调用new会调拥operator new,operatornew (和operator delete)的调用通常代价昂贵,因为它们将通常会调拥底层操作系统,系统调用的速度一般比进程内函数调用的速度慢。因此我们应该尽量少使用系统调用。

可以用over-eager evaluation(超急评估)策略,理由是我们现在必须增加数组大小以容纳索引i,“locality of reference法则”建议我们未来可能还会增加数组尺寸以容纳比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位置上的数组元素;
}

上述做法在每次数组需要扩张时,分配两倍内存。先前的使用方式只要分配一次内存就行。

STL中的 vector分配内存就是用了prefetching(预先取出)这种方法。

总结:较佳的速度往往导致较大的内存成本。cache会消耗较多内存,但可以降低哪些已被缓存的结果的重新生成时间。prefetch需要一些空间来放置被预先取出的东西,但可降低访问它们所需的时间。当你必须支持某些运算而其结果并不总是需要时, lazy evaluation可以改善程序效率。当你必须支持某些运算而其结果几乎总是被需要或被常常被多次需要时, over-eager可以改善程序效率。

版权声明:本文为博主原创文章,未经博主允许不得转载。

原文地址:https://www.cnblogs.com/ruan875417/p/4785423.html