代码优化这种东西,肯定是经验之谈,即使你对代码的执行细节理解的很清楚,没有实际的经验体会也不会对之有多大的感觉。看书本人虽然觉得是揠苗助长,但是这估计就类似于打疫苗吧。要是艾滋有疫苗现在估计就没有世界艾滋日了。所以,看看书打打疫苗还是很有好处的。
代码优化很多就是些避免冗余计算,但是场合不同,有些可能你都从没有考虑过需要这样的优化。我打算把我从提高c++性能的编程技术上面看到的代码优化总结一下,随便扯几下。
例子一,用缓存计算结果优化循环。
原始代码,
1 | for (...; !done; ...) done = patternMatch(pat1, pat2, isCaseSenstive()); |
优化代码,
1 | bool isSenstive = isCaseSenstive(); |
这个例子比较简单,一般情况大家都能优化循环,去掉循环内部的重复的函数调用和计算等。
例子二,预先计算某些结果以优化后面的计算。
比如,你需要将一个字符串转换为大写的,你是对字符串循环一次,循环内部调用toupper了,还是预先计算出toupper的结果,以后每次查表了。
优化后的代码应该是类似于这样的。
for (char p = header; p; ++p) p = uppercaseTable[p];
你所需要做的就是在初始化时候构造出uppercaseTable这张大小为256的表,如果你需要经常做大写转换无疑能不错的提高性能。
例子三,降低灵活性来换取速度。
书上只举了一个例子,为ip地址分配内存。我们知道ip4的地址不会超过15个字节,即使ip6的地址也不是很长。如果每次,我们动态分配内存去容纳地址,这肯定是非常浪费的事情。如果你需要容纳的地址数目是有限的,那么我们完全可以静态分配数组来容纳地址。全局new和delete的操作是非常昂贵的,这不仅仅在堆内存分配过程的复制上,线程同步机制也消耗了很多cpu周期。这只是个小小的例子而已,灵活性有的时候根本没必要。
例子四,用80-20规则优化常用代码路径速度。
这个例子的内容说起来比较麻烦,其实就是利用c语言的短路逻辑判断,执行较少的代码。
比如,if (e1 || e2)和if (e1 e2)这样的代码。通过,优化可以提高速度。
假设c1为e1执行的代价,p1为e1为真的概率,c2为e1执行的代价,p2为e2为真的概率。
那么可以计算出if (e1 || e2)的代价期望,同样可以计算出if (e2 || e1)的代价期望,选择较小的即可了。
例子五,延迟计算。
有些计算你可能并不需要,那么就请等到你真正需要的时候再计算吧。
1 | int route(Msg* msg) |
这种代码明显应该把对象upstream的定义放在if内部,因为有些对象的构造和析构可能会花费很大代价。
不过,我好像记得有公司内部的代码规范禁止在构造函数和析构函数作有意义的初始化和释放,而是自己再写个Init和UnInit函数来代替构造函数和析构函数的本职作用。我也不知道哪样好点,如果是那样的话,本例子就对他们没有意义了。呵呵。
例子六,去掉无用计算。
例子的内容是用成员初始化列表代替成员的默认构造函数的调用,没有什么多说的。
例子七,恰当利用系统的缓存机制。
比如,下面结构体的定义。
struct X { int a; char b[4096]; int c;};
应该改成struct X { int a; int c;char b[4096]; };
原因是cpu缓存可能就128个字节,那么加载a的时候可能一次缓存了连在一起的128个字节。由于a和c之间距离比较远,那么c就不在缓存行里面了。如果,你需要再访问c就会造成缓存失败,就需要重新加载缓存。这种降低缓存命中率的事情确实不怎么好。但是,你也不知道编译器会不会调整内存结果,把a和c默认连在一起了。我觉得还是不要做这种假设吧,这完全是改变编程者的意图了。
例子八,去掉没必要的堆内存使用,改为堆栈内存。
例子九,谨慎使用库。
比如,string的使用很多情况就比默认的char数组慢很多。其内部七七八八的实现不知道做了多少工作了,而作为调用者可能你都不怎么清楚。就比如很多时候我们根部不需要线程同步,但是string的内部实行可能考虑了线程同步,那么对于我们这完全就是个浪费了。string.h内部的那些字符串操作函数,应该是没有考虑线程同步的,这个时候显然就快很多了。
例子十,请打开编译器优化选项。
因为很多编译器优化选项不是默认打开的,如果你没有打开,那么编译器是不可能为你做全力的优化的。