4.效率
条款16:谨记80-20法则
80-20法则:一个程序80%的资源用于20%的代码上。
这个条款说的就是,如果要分析(profile)你的程序,聪明的去找百分之20的代码,而不是在另外百分之20上浪费时间。
条款17:考虑使用lazy evaluation
缓释评估,这里的例子很生动,我记得我第一次看到类似的博客,也是用这个例子解释的。
如果你爸妈叫你打扫卫生,你肯定不会马上打扫。事实上,清理房间是万不得已的情况下才行动的(一般都是爸妈真的过来了,听到脚步)。如果你足够幸运,这次爸妈没来检查,你就不必清理房间,而躲过一劫。
这个条款介绍了4种途径实现lazy evaluation:
引用计数(Reference count)
stdc++中的sting就是用引用计数实现的,原理和cow很像。刚开始分配好的时候,不需要给被拷贝的字符串开辟一个新的空间复制所有的字符。而是让他们指向同一个空间,只是要加以记录,也就是引用计数加一。但是当其中一个字符串内容被改变的时候,就需要开辟新空间,以确保两个字符串不互相影响。
对于string的引用计数,我再我windows机器和wsl(linux)上都测试了一下。都不是用的引用计数实现。测试代码如下
区分读和写
同上一个引用计数的例子。如果对一个使用引用计数的字符串。对她进行读操作,不会产生复制行为。但是进行写操作,需要开辟新空间。
但是我们自己写的接口,很难设计成能区分读和写的。使用条款30所描述的proxy classes,可以延缓决定究竟是读还是写,直到能确定答案为止
lazy fetch(缓式取出)
lazy fetch面对是一个巨大的对象。如果一个大对象必须从数据库中取出。但是一个函数利用到的就只有他的一小部分。那么其他部分可以完全不用取出。
lazy expression evaluation(表达式缓评估)
对于c++内置类型,当然不太需要缓评估。例如int double,运算都不是太慢。
但是对于大型矩阵运算。一次矩阵加法,可能要10000次普通加法。所以,缓释评估是有必要了。
首先举个最简单的缓释评估的例子。
通过上面例子可以看出,看到一个表达式之后,没必要马上求出结果。在需要的时候再计算最好。
在成员值被改变的时候,也要马上计算出结果。这样看来,缓式表达还是需要维护很多情况的
摘要
上面4中例子显示,lazy evaluation在很多地方都有用途。可以避免很多不必要的操作。尽管如此,它并非永远是好主意,如果你必须要求出结果,那直接计算出结果会好很多。
当然,这个条款不是c++的专属,它是一种技巧,其他语言也可以用到
条款18:分期摊还预期的计算成本
上一个条款讲到了,lazy evaluation,缓评估。也说到了,并不是所有情况下,lazy都是好的。
这个条款,我们主要讲的就是,over-eager evalution,超急评估。
在设计一个数据收集表的class,这个类有三个方法,需要实时返回max min 和 avg。如果采用lazy的方式,这些方法都返回某种数据结构,在真正需要数据的时候,数据结构才会返回数字。
如果用over eager的方式去做,每次插入一个数字的时候,我们就计算好max和min以及avg,需要的时候直接取出来用即可。这其实算是一种cache。
Cache是一种over eager的实现方式,很多算法题也需要memory的方法加快速度,一般都是使用std::unordered_map作为cache。当然,除了cache还有一种方法也是over eager,那就是prefetch(预先取出)
perfetch就和上面条款的lazy fetch对应了,很有意思。通常情况下,操作系统读取多个小块的速度是慢于一个大块的。所以读一个小块的时候,将旁边的小块一起读出,就像读一个大块一样。
stl::vertor设计的内存分配器,在每次数据不够的时候,都会分配两倍的数据,这其实也是种over eager的情况。因为如果每次只加一,之后很可能又会需要新分配空间,operator new需要系统调用,但是如果分配两倍的话,之后就只需要函数调用了。这也会快很多。也是种典型的空间换时间
条款19:了解临时对象的来源
临时变量是没有名字的变量,这就说明,局部变量不是临时变量。
临时变量一般出现在两个地方,一个是隐式转型的时候,二是当函数返回一个对象的时候。
第一种情况在传递参数的时候经常会出现,只有当你的参数是by value 传递的时候 或者是 by reference to const 传递 ,并且这时候传入一个其他类型的对象,编译器会产生临时对象。需要注意的是,如果参数是by reference to non const的时候不会产生一个临时对象,因为这个产生的临时对象是可以被改变的,但是它是个临时对象,它被改变了之后,原来传进来的数据是不会被改变的。这就会出问题
第二种情况时函数返回一个对象的时候,这种情况大多数人应该都知道。这种临时对象十分消耗成本,可以通过之后的条款优化
条款20:协助完成“返回值优化”
有些情况下,无法避免产生一个新对象。c++允许编译器进行返回值优化,试不产生临时对象。下面两个代码中,上面那个会产生临时对象,下面那个不会产生
可以看到,其实两种写法区别不大,但是如果像下面这么写,编译器就可以利用返回值优化的形式减少一个临时对象的产生。
条款21:利用重载技术避免隐式类型转换
这个条款其实和上面提到的,使用非成员函数实现操作符是有点冲突的。使用非成员函数实现操作符就是利用了可以隐式转换的便利之处,只要写一个函数就可以让所有其他的操作合法。
但是这里却是要增加重载函数,来避免这种隐式转换,从而提高效率。其实两种说法都是对的,只是看你用在哪个地方,如果注重效率,不注重代码篇幅,多重载是可以接受的。但是如果注重80-20法则,想要代码篇幅减少,那么这里只能接受隐式转换
条款22:考虑以操作符符合形式来取代其独生形式
这个条款大致是说多使用a+=b操作,而不是a=a+b。 因为前者不会产生一个新的对象,但是后者至少会产生一个新对象。伴随着构造和析构,会减慢程序运行的时间
条款23:考虑使用其他库
在当前环境下,很多需求以及被包装在了一些很好的库里面。甚至有些需求被几个不同的库实现。
这里拿了stdio和iostream对比,都是IO库,但是如果注重效率,使用stdio是对的。如果注重格式,使用体验,那么使用iostram更好。
多去考虑是否可以使用其他库
条款24:了解virtual function,multiple inheritance,virtual base classes,runtime type identification的成本
虚函数很好,动态绑定,c++多态基本上是靠虚函数实现的。毋庸置疑的是,虚函数肯定会给类增加更多的成本,来处理多态的情况。这个条款也介绍了下一般编译器是如何实现虚函数的。
为了实现虚函数,会多出两个结构,一个是vtbl,一个是vptr。 vtbl就是虚函数表,类似一个函数指针数组。保存了当前类应该调用的虚函数,vptr也就是虚函数表指针,指向这个虚函数表。我们无需给每个对象都实现一个vtbl只需要每个类一个即可,在对象之中我们需要保存的就只有vptr。每个函数都有自己再vtbl上的索引,这样,我们调用一个虚函数,就可以类似为如下过程
除了上面的步骤使得虚函数成本增加,另一个问题是虚函数不能被inline。这间接导致了性能问题。由于虚函数调用是动态的,如上面所示的方法,编译器没法知道如何给函数加inline
最后说下RTTI,也就是运行时识别技术。这个其实和虚函数一样,也是运行时动态识别的。所以也可以直接用vtbl保存信息即可。直接在vtbl特定位置加上一个type_info信息,使用RTTI的时候,直接vtbl中查找即可
Last updated
Was this helpful?