More Effective C++读书笔记(四)

效率

在用C++写出高效地程序之前,必须认识到C++本身绝对与你所遇到的任何性能上的总是无关。如果想写出一个高效的C++程序你必须首先能写出一个高效的算法

条款16:牢记8020准则

80-20 准则说的是大约 20%的代码使用了 80%的程序资源大约 20%的代码耗用了大约 80%的运行时间;大约 20%的代码使用了 80%的内存;大约 20%的代码执行 80%的磁盘访问;80%的维护投入于大约 20%的代码上;通过无数台机器、操作系统和应用程序上的实验这条准则已经被再三地验证过。80-20 准则不只是一条好记的惯用语,它更是一条有关系统性能的指导方针,它有着广泛的适用性和坚实的实验基础。

    当想到 80-20 准则时,不要在具体数字上纠缠不清,一些人喜欢更严格的 90-10 准则,而且也有一些试验证据支持它。不管准确地数字是多少,基本的观点是一样的:软件整体的性能取决于代码组成中的一小部分。

    本章主要讲解如何找到影响性能瓶颈的20%的代码的位置。提高效率并不难,难得是如何找到性能的正确瓶颈。方法不外乎有两种,一是猜测或屏经验判断二是通过profile工具准确测算,这两种方法,当然是第二种方法更科学,更可信。利用好profile工具,提供最有效的数据进行测试,找到程序的瓶颈是一个程序员需要掌握的高级技巧之一

 

条款17:考虑使用懒惰计算法lazy evaluation(缓式评估)

懒惰计算法的含义是拖延计算的时间,等到需要时才进行计算其作用为:能避免不需要的对象拷贝,通过使用operator[]区分出读写操作,避免不需要的数据库读取操作,避免不需要的数字操作。但是,如果计算都是重要的,懒惰计算法可能会减慢速度并增加内存的使用

1.从效率的观点来看,最佳的计算就是根本不计算。关键是要懒惰

2.当你使用了lazy evaluation后,采用此种方法的类将推迟计算工作直到系统需要这些计算的结果。如果不需要结果,将不用进行计算

3.引用计数

4.区别对待读取和写入:残酷的事实是我们如何判断是读取操作还是写入操作,通过使用lazy evaluation和条款M30中讲述的proxy class,我们可以推迟做出是读操作还是写操作的决定

5.Lazy Fetching(懒惰提取)

6.Lazy Expression Evaluation(懒惰表达式的计算)

7.应用:能避免不需要的对象拷贝,通过使用operator[]区分出读操作,避免不需要的数据库读取操作,避免不需要的数字操作

 

条款18:分期摊还期望的计算

1.这个条款的核心就是over-eager evaluation(过度热情计算法):在要求你做某些事情以前就完成它们

2.隐藏在over-eager evaluation后面的思想是如果你认为一个计算需要频繁进行,你就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求时的开销

3.采用over-eager最简单的办法就是caching(缓存)那些已经被计算出来而以后还有可能需要的值

4.caching是一种分摊期望的计算开销的方法,prefetching(预提取)是另一种方法。你可以把prefetch想像成购买大批商品而获得的折扣,以空间换效率。

5.当必须支持某些操作而不总需要其结果时,可以使用懒惰计算法提高程序运行效率;当必须支持某些操作而其结果几乎总是被需要或不止一次地需要时,可以使用过度热情算法提高程序运行效率

6.注意STL明确要求“.”和“*”对iterators必须有效,所以*it.second虽然语法上笨拙,却保证能够有效运行。

 

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

1.在C++中,真正的临时对象是看不见的,它们不出现在源代码中。建立一个没有命名的非堆对象会产生临时对象。这种未命名的对象通常在两种条件下产生:为了使函数调用而进行隐式类型转换和函数返回对象时。

2.仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生

3.C++语言禁止为非常量引用产生临时对象的原因:当程序员期望修改非临时对象时,对非常量引用进行的隐式类型转换却修改临时对象,这是不希望看到的,即该修改的没修改,却修改了临时对象,导致结果不是预期的。

4.像operator+必须返回一个对象,每当调用时,便得为此对象付出构造和析构成本。对于此特殊函数,你可以改用一个类似的函数operator+=免掉这份成本。

5.临时对象是有开销的,所以你应该尽可能地去除它们,然而更重要的是训练自己寻找可能建立临时对象的地方。任何时候只要见到常量引用参数,就存在建立临时对象而绑定在参数上的可能性;在任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放)

 

条款20:协助完成返回值优化

   返回对象时的开销会比较大,会调用对象的构造和析构函数,但是当一个函数必须要返回对象时,这种构造和析构造成的开销是无法消除的。那么还能优化么?

以某种方法返回对象,能让编译器消除临时对象的开销,这样编写函数通常是很普遍的。这种技巧是返回constructor argument而不是直接返回对象。你可以这样做:

inline const Rationaloperator*(const Rational& lhs, const Rational& rhs)

{

    return Rational(lhs.numerator() *rhs.numerator(),

                  lhs.denominator() *rhs.denominator());

}

仔细观察被返回的表达式。它看上去好象正在调用Rational的构造函数,实际上确是这样。你通过这个表达式建立一个临时的Rational对象,

Rational(lhs.numerator() *rhs.numerator(),  lhs.denominator() *rhs.denominator());

并且这是一个临时对象,函数把它拷贝给函数的返回值。

返回constructor argument而不出现局部对象,这种方法还会给你带来很多开销,因为你仍旧必须为在函数内临时对象的构造和释放而付出代价,你仍旧必须为函数返回对象的构造和释放而付出代价。但是你已经获得了好处。C++规则允许编译器优化不出现的临时对象(temporary objects out of existence)。因此如果你在如下的环境里调用operator*:

Rational a = 10;

Rational b(1, 2);

Rational c = a * b;                          // 在这里调用operator*

编译器就会被允许消除在operator*内的临时变量和operator*返回的临时变量。它们能在为目标c分配的内存里构造return表达式定义的对象。如果你的编译器这样去做,调用operator*的临时对象的开销就是零:没有建立临时对象。你的代价就是调用一个构造函数――建立c时调用的构造函数。而且你不能比这做得更好了,因为c是命名对象,命名对象不能被消除(参见条款M22)。不过你还可以通过把函数声明为inline来消除operator*的调用开销。

它有一个专属名称:return value optimization,这足以反映出它是多么被广泛运用。

 

条款21:通过重载避免隐式类型转换

隐式类型转换将产生临时对象,从而带来额外的系统开销

解决办法是使用重载,以避免隐式类型转换要注意的一点是在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型的参数(这条规定是有道理的,如果没有的话,程序员将能改变预定义的操作,这样做肯定吧程序引入混乱的境地)

class UInt

{

public:

    const UIntoperator+(const UInt& lrs, const UInt& hrs);

};

 

UInt a, b, c;

a = b + 10;

在计算b+10的时候,编译器会将10隐式的转为UInt类型进行计算,这种情况下可以通过重载操作法来避免出现隐式转换的开销。当然是必要时候才会采取这种方法,如果不是性能瓶颈,那么有可能造成重载函数过多,而维护起来会很不方便。

    注意一种情况const UPInt operator+(intlhs, int rhs);   这种情况是错误的。在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型(user-definedtype)的参数

另外,牢记80-20规则,没有必要实现大量的重载函数,除非有理由确信程序使用重载函数后整体效率会有显著提高

 

条款22:考虑用运算符的赋值形式取代其单独形式

1.从零开始实现operator+=-=,而operator+和operator-则是通过调用前述的函数提供自己的功能。使用这种设计方法,只用维护operator的赋值(复合)形式就行。而且如果假设operator赋值形式在类的public接口里,这就不用让operator的单独形式成为类的友元

2.如果你不介意把所有的operator的单独形式放在全局域里,那就可以使用模板来替代单独形式的函数的编写:

template< typename T >

const T operator+( const T& lhs, const T& rhs)

{

 return T(lhs)+= rhs;

}

const T operator-( const T& lhs, const T& rhs)

{

 return T(lhs)-= rhs;

}

3.这里指出三个效率方面的问题:一、总的来说operator的赋值形式比其单独形式效率更高,因为单独形式要返回一个新对象,从而在临时对象的构造和释放上有一些开销。operator的赋值形式把结果写到左边的参数里,因此不需要生成临时对象容纳operator的返回值;二、提供operator的赋值形式的同时也要提供其标准形式,允许类的客户端在便利与效率上做出折衷选择;独身形式较易撰写、调试、维护,并在80%的时间内供应充足可接受的性能。复合形式效率较高,而且对汇编语言程序员比较直观。三、涉及到operator单独形式的实现。自古以来匿名对象总是比命名对象更容易被消除,所以当你面临命名对象或临时对象的抉择时,最好选择临时对象。它应该绝不会比其命名兄弟好用更多成本,反倒是极有可能降低成本。

4.身为一位程序库设计者,你应该将复合版本以及独身版本都提供;身为一位应用软件开发者,如果性能是重要因素的话,你应该考虑以复合版本操作符取代其独身版本

 

条款23:考虑变更程序库

1.程序库的设计就是一个折衷的过程。理想的程序应该是短小的、快速的、强大的、灵活的、可扩展的、直观的、普遍适用的、具有良好的支持、没有使用约束、没有错误的。当然这也是不存在的。为尺寸和速度而进行优化的程序库一般不能被移值。具有大量功能的程序库不会具有直观性。没有错误的程序库在使用范围上会有限制。所以不同的设计者给这些条件赋予了不同的优先级。他们从而在设计中牺牲了不同的东西,因此一般两个提供相同功能的程序库却有着完全不同的性能特征

2.程序库必须在效率和功能等各个方面有各自的权衡,因此在具体实现时应该考虑利用程序库的优点例如程序存在I/O瓶颈,就可以考虑用stdio替代iostream

3.注意这章的例子,对ifdef的运用。

 

条款24:理解虚拟函数、多继承、虚基类和RTTI(运行时期类型辨识)所需的代价

1.虚函数所需的代价:必须为每个包含虚函数的类的virtual table留出空间;每个包含虚函数的类的对象里,必须为额外的指针付出代价(vptr);实际上放弃了使用内联函数。

2.Class’s vtbl被产生于“内含其第一个non-inline,non-pure虚函数定义式”的目标文件中。

3.inline意味着“在编译期,将调用端的调用动作被调用函数的函数本体取代”,而virtual则意味着“等待,直到运行时期才知道哪个函数被调用”。当编译器面对某个调用动作,却无法直到哪个函数改被调用时,你就可以了解为什么他们没有能力将该函数调用加以inlining了。避免将虚函数声明为inline。

4.多继承时,在单个对象里有多个vptr(一个基类对应一个)它和虚基类一样,会增加对象体积的大小RTTI能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息,让我们查询这些信息被存储在类型为type_info的对象里,可以通过typeid操作符访问到一个类的typeid对象通常,RTTI被设计为在类的vbtl上实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值