阿龙的学习笔记---More Effective C++---第四章:效率

  本章阐述能够帮助程序提高效率的几个tips。

  • 16. 谨记80-20法则
    • 80-20法则是说:一个程序的80%的资源都用在20%的代码上。重点就是:软件的整体性能几乎由其构成要素的一小部分决定。
    • 所以提高性能重要的是:找到瓶颈。大部分人的错误做法就是 “猜” ,这并不能引导你走向20%的瓶颈。
    • 好的方法是借助程序分析器,测量你所在意的数据。比如时间效率,那么就使用分析器分析每个程序段运行的时间、或者某些函数被调用的次数等。
    • 分析器需要好的数据来分析,假如运行出问题,那么将问题复现,无法重现的数据意义不大。所以尽可能多的数据来分析。
  • 17. 考虑使用 lazy evaluation(缓式评估)
    • lazy evaluation缓式评估 相当于 “拖延战术”,就是直到某些运算或操作一定要进行不可的时候再去运算。如果某些运算结果一直不被需要,那么可以一直拖下去。
    • 比如条款29所讲到的,如果要复制一个副本的效率很低,我们可以直接让两个对象指向同一个东西,如果一直是读操作不会更改这个值,则没问题,万一要写操作,那么再复制也不迟。
    • 再比如说:程序中使用大型对象,假如取出之后只用到的一小点部分,但是所有的成员都要被取出到内存中。那么我们可以将这个大型对象中成员改一下,将里面的成员变量改成指针,需要用到哪一个再从内存中取出。
    • 再比如说:假如一个矩阵加法运算,每次运算的运算量很大,但有可能这个运算的结果我们用不到。那么可以考虑当结果用到的时候,再进行计算。
    • lazy evaluation也不一定是有效的,如果你的所有计算都是必须的,那么加上lazy evaluation不会带来任何好处,反而还可能让结果更坏。
  • 18. 分期摊还预期的计算成本 — 超急评估(over-eager evaluation)
    • 这一条与上一条刚好相反,这一条是说:在计算到来之前就提前做完。那么在什么情况下好呢??(我理解为是在: 计算被调用次数 > 计算结果变化次数 时) 。
    • 考虑一个数据统计类,其中有max、min、avg函数返回最大、最小、平均值。如果这些函数调用的次数很频繁,那么在使用前就计算好(比如只要数据改变就计算),那么计算的成本就被分摊到每次调用,如果调用很频繁,则不用每次都再次计算。这就降低了计算成本。
    • 另一种使用情况是:就如操作系统中的局部性原理,假如涉及到磁盘读取,那么需要读取一个数据时,将其附近的数据一并提前读取,之后用到的可能性会很大,那么则分摊了一次磁盘读取的成本。 再比如:数组的operator []操作,假如索引值超过数组长度,那么则需要operator new[] 动态申请,这个操作是费时的。如果我们每次申请两倍的空间,那么则换取了效率。
  • 19. 了解临时对象的来源
    • 这里说的 临时对象 不是一个swap函数中的temp,对编译器来说那是局部变量。临时变量不在你的代码中,但运行过程中会产生的。 两种情况,一是隐式类型转换来保证函数调用的参数的类型匹配;二是数返回对象。这些对象的构造和析构成本是会影响程序效率的。

    • 首先,第一种情况:当一个函数参数为 const string& 类型,但是你传入了一个char*类型。编译器会发现类型不匹配,并且帮你隐式转化来匹配。这时会有一个临时string对象由string::string(char*);构造函数来构造出来,并且在函数调用完成后再被销毁。

      并且只有pass-by-value和pass-by-reference-to-const编译器会隐式转换,为啥reference-to-non-const 不会呢??如果不是const,那么则可能改变这个reference,如果要转换了一定是临时变量,而不是原来的值,那么无意义。

    • 第二种情况:在函数返回一个对象by-value时,一定会产生一个临时对象。返回值优化下一条会讲。

    • 结论是:尽可能看出哪里会产生临时变量,并尽可能消除。

  • 20. 返回值优化
    • 前文提到,如果返回一个对象会产生临时变量而带来构造函数和析构函数的成本。但是在一些情况下,需要返回一个对象时,的确不能返回指针或引用。

    • 对于返回对象,也有优化方式。如果我们返回一个Constructor argument(构造函数对象??),那么编译器会进行优化。

      例如分数相乘:

      const Rational operator*(const Rational &a,const Rational &b){
      	return Rational(a.numerator() * b.numerator() , a.denominator() * b.a.denominator() );
      }
      

      编译器会进行优化,调用 C = A*B 时,编译器会直接将这个构造函数针对C,而不会产生额外的临时变量。

      这也在于编译器的设计者,不是每个编译器都有这个功能。英文叫 return value optimization。

  • 21. 利用重载技术避免隐式类型转换。
    • 在函数的参数调用时,如果你传入了不符合的参数,那么编译器可能会发生隐式转换来转换成参数列表中的类型。这样会带来临时变量的开销。
    • 如果在设计函数时,就将所有可能的类型包含,多写几个重载函数,则可以避免一部分的开销。
  • 22. 考虑操作符复合形式(op= )来代替单独操作符(op)
    • 比如 + 和 +=,我们最好使用 += 。 假如 a = a + 1; 那么会有return value,还会多赋值操作。如果使用+=,那么直接传入引用,在其上加一个数字即可。
    • 在设计 op+op+= 时,也可以在 op+ 中使用 op+= 来提高效率。
      tamplate<class T>
      T operator+( T a, T b){
      	return T(a) += b;
      }
      
      这里的返回值就用到了上一条提到的方法。
  • 23. 考虑使用不同的程序库
    • 比如说 iostream 和 stdio.h,都可以用来输出,其效率、安全性等都是不一样的。所以,选用适合的程序库。
  • 24. 了解 虚函数、 多重继承、虚基类、RTTI带来的成本
    • 首先虚函数机制由 虚函数表虚函数指针来保证,这就带来了一些成本:一是每个拥有虚函数的类都会占用一个虚函数表的空间;二是每个对象都会占用一个虚指针的空间(虽然一个指针可能不大);三是虚函数不能inline,所以放弃inline可能会降低效率。

      但是虚函数的调用成本不会很大,就与使用一个函数指针来调用差不多。

    • 多重继承中采用虚基类继承的方式可能也会使得对象中多增加指向虚基类的指针。不深究了吧,总之就是会占用空间。

    • RTTI让我们能再运行时知道对象或者class的信息,所以必须有一些地方来存贮这些信息,使用这个机制就会带来一些些成本。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值