《More Effective C++》总结笔记(三)

420 阅读9分钟

效率

条款16:谨记80-20法则

  • 80-20法则说:一个程序80%的资源用于20%的代码身上。是的。80%的执行时间花在大约20%的代码身上,80%的内存被大约20%的代码使用,80%的磁盘访问动作由20%的代码执行,80%的维护力气花在20%的代码上面。
  • 不必拘泥于法则的数字,其基本重点是:软件的整体性能几乎总是由其构成要素(代码)的一小部分决定。
  • 从某个角度看,80-20法则暗示,大部分时候你所产出的代码,其性能坦白说是平凡的,因为80%的时间中,其效率不会影响系统整体性能。或许这不至于对你的自尊心造成太大打击,但应该多少会降低你的压力。从另一个角度看,这个法则暗示,如果你的软件有性能上的问题,你将面临悲惨的前景,因为你不只需要找出造成问题的那一小段瓶颈所在,还必须找出办法来大幅提升其性能。这些工作中,最麻烦的还是找出瓶颈所在。
  • 找性能瓶颈的方法不能靠猜。可行之道是完全根据观察或实验来识别出造成你心痛的那20%代码。而辨识之道就是借助某个程序分析器。然而并不是任何分析器都足堪大任,它必须可以直接测量你所在意的资源。

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

  • 一旦你采用lazy evaluation,就是以某种方式撰写的你classes,使它们延缓运算,直到那些运算结果刻不容缓地被迫切需要为止。如果其预算结果一直不被需要,运算也就一直不执行。
  • lazy evaluation在许多领域中都可能有用途:可避免非必要的对象复制,可区别operator[]的读取和写动作,可避免非必要的数据库读取动作,可避免非必要的数值计算动作。

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

  • 此条款背后的哲学可称为超急评估(over-eager evaluation):在被要求之前就先把事情做下去。
  • over-eager evaluation背后的观念是,如果你预期程序常常会用到某个计算,你可以降低每次计算的平均成本,办法就是设计一份数据结构以便能够极有效率地处理需求。
  • 其中个最简单的一个做法就是将“已经计算好而有可能再被需要”的数值保留下来(所谓caching)。另一种做法则是预先取出(prefetching)。prefetching的一个典型例子就是std的vector数组扩张的内存分配策略(每次扩大两倍)。
  • 条款17和18看似矛盾,实际都体现了计算机中一个古老的思想:用空间换时间。结合起来看,它们是不矛盾的。当你必须支持某些运算而其结果并不总是需要的时候,lazy evaluation可以改善程序效率。当你必须支持某些运算而其结果几乎总是被需要,或其结果常常被多次需要的时候,over-eager evaluation可以改善程序效率。

条款19:了解临时对象的来源

  • C++真正的所谓的临时对象时不可见的——不会在你的源代码中出现。只要你产生一个non-heap object而没有为它命名,便诞生了一个临时对象。此等匿名对象通常发生于两种情况:一是当隐式类型转换被施行起来以求函数调用能够成功;二是当函数返回对象的时候。
  • 任何时候只要你看到一个reference-to-const参数,就极可能会有一个临时对象被产生出来绑定至该参数上。任何时候只要你看到函数返回一个对象,就会产生临时对象(并于稍后销毁)。学些找出这些架构,你对幕后成本(编译器行为)的洞察力将会有显著地提升。

条款20:协助完成“返回值优化(RVO)”

  • 如果函数一定得以by-value方式返回对象,你绝对无法消除之。从效率的眼光来看,你不应该在乎函数返回了一个对象,你应该在乎的是那个对象的成本几何。你需要做的,是努力找出某种方法以降低被返回对象的成本,而不是想尽办法消除对象本身。
  • 我们可以用某种特殊写法来撰写函数,使它在返回对象时,能够让编译器消除临时对象的成本。我们的伎俩是:返回所谓的constructor arguments以取代对象。考虑分数(rational numbers)的operator*函数,一个有效率而且正确的做法是:
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
  • C++允许编译器将临时对象优化,使它们不存在。于是如果你这样调用operator*:
Rational a = 10;
Rational b(1,2);
Rational c = a * b;
  • 你的编译器得以消除“operator*内的临时对象”及“被operator*返回的临时对象”。它们可以将return表达式所定义的对象构造于c的内存内。
  • 你可以将此函数声明为inline,以消除调用operator*所花费的额外开销。这就是返回一个对象最有效率的做法。

条款21:利用重载技术(overload)避免隐式类型转换(implicit type conversions)

  • 考虑以下代码:
class UPInt { // 这个class用于无限精密的整数
plublic:
    UPInt();
    UPInt(int value);
    ...
};
const UPInt operator+(const UPInt& lhs, const UPInt& rhs);
UPInt upi1, upi2;
...
UPInt upi3 = upi1 + upi2;
upi3 = upi1 + 10;
upi3 = 10 + upi2;
  • 因为有隐式转换,所以以上语句都能执行成功。但这样执行会产生临时对象,这对执行效率是有影响的。
  • 如果有其他做法可以让operator+在自变量类型混杂的情况下呗调用成功,那便消除了了类型学转换的需求。如果我们希望能够对UPInt和int进行加法,我们需要做的就是将我们的意图告诉编译器,做法是声明数个函数,每个函数有不同的参数表:
const UPInt operator+(const UPInt& lhs, const UPInt& rhs); // 将UPInt和UPInt相加
const UPInt operator+(const UPInt& lhs, const int& rhs); // 将UPInt和int相加
const UPInt operator+(const int& lhs, const UPInt& rhs); // 将int和UPInt相加
  • 但是不要写出以下函数:
const UPInt operator+(const int& lhs, const int& rhs);
  • 因为,C++存在很多游戏规则,其中之一就是:每个“重载操作符”必须获得至少一个“用户定制类型”的自变量。int不是用户定制类型,所以我们不能够将一个只获得int自变量的操作符加以重载,这会改变ints的加法意义。

条款22:考虑以操作符复合形式(op=)取代其独身形式(op)

  • 要确保操作符的复合形式(例如,operator+=)和其独身形式(例如,operator+)之间的自然关系能够存在,一个好方法就是以前者为基础实现后者:
const Rational operator+(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs) += rhs;
}
  • 一般而言,复合操作符比其对应的独身版本效率高,因为独身版本通常必须返回一个新对象,而我们必须因此负担一个临时对象的构造和析构成本。至于复合版本则是直接将结果写入其左端自变量,所以不需要产生一个临时对象来放置返回值。
  • 操作符我复合版本比其对应的独身版本有着更高效率的倾向。身为一位程序库设计者,你应该两者都提供;身为一位应用软件开发者,如果性能是重要因素的话,你应该考虑以复合版本操作符取代其独身版本。

条款23:考虑使用其他程序库

  • 不同的设计者面对不同的特性会给予不同的优先权。他们的设计各有不同的牺牲。于是,很容易出现“两个程序库提供类似机能,却又相当不同的性能表现”的情况。
  • 所以一旦你找出程序的性能瓶颈(通过分析器),你应该思考是否有可能因为改用另一个程序库而移除了那些瓶颈。

条款24:了解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本

  • 虚函数的第一个成本:你必须为每个拥有虚函数的class耗费一个vtbl空间,其大小视虚函数的个数(包括继承而来的)而定。
  • 虚函数的第二个成本:你必须在每一个拥有虚函数的对象内付出“一个额外指针”的代价。
  • inline意味着在编译期将调用端的调用动作被调用函数的函数本体取代,而virtual则意味着直到运行期才直到哪个函数被调用。因此虚函数的第三个成本就是:你事实上等于放弃了inlining。
  • 多重继承往往导致virtual base classes(虚拟基类)的需求。virtual base classes可能导致对象内的隐藏指针增加。
  • RTTI让我们得以在运行期获得objects和classes的相关信息,它们本存放在类型为type_info的对象内。你可以利用typeid操作符取得某个class相应的type_info对象。
  • RTTI的设计理念是:根据class的vtbl来实现。举个例子,vtbl数组中,索引为0的条目可能内含一个指针,指向“该vtbl所对应的class”的相应的type_info对象。运用这种实现方法,RTTI的空间成本就只需在每一个class vtbl内增加一个条目,再加上每个class所需的一份type_info对象空间。