项目接近尾声,我们开始做各种各样复杂一些的测试,优化在这个时候成为一个比较明显的问题。
也许有的朋友会说,为何一开始不考虑优化?作为一个老程序员,基本的一些常识还是有的,比如不去反复调用一些初始化操作,GetCount或者是GetSize这样的操作不放在循环里面进行,但是运行性能问题还是会发生。在最后的时候优化,算是一个比较好的时机,因为过早优化会没有目的性,既然客户端没有进行实际的调用,怎么知道那些操作会带来性能问题呢,只有跑起来才知道。
我们项目是一个组件程序,通过COM接口被其他程序使用,设计为了简单,将一些数据放在注册表里存放,另外其他大部分数据都是读取一个格式化的文本文件(EDS)来获得的。程序的整个结构是单线程的,同一个进程中,任何一个复杂操作都会阻塞其它调用的进行。
代码优化的重点是放在初始化这个阶段,因为我们需要读文件来构造整个系统,这其中牵扯到注册表操作以及文件操作和对文件解析生成特定的实例(instance)。
注册表的优缺点暂不评论,但是它的性能真的是很成问题,注册表是典型的key-value结构,按理说读取速度应该很快,但是它真的很牵扯性能,这个地方的优化做了一些,但是本质上没办法做更多的优化,个人倒是比较喜欢一些简单的数据库作为程序后端存储,比如很多公司都用到的sqlite引擎,支持sql语言查询,性能还算是不错,应该可以作为一个很好的替代品。问题是类似东西可能有license引用的问题,这个就比较复杂了。
我们组里另外一个同事针对注册表操作做了一些改进,估计这方面也不能做的更好了。所以注册表方面暂借不管,主要精力放在内部代码改进。
首先必须要做的就是收集数据,看看那些函数调用费时间。好在我已经有现成的log以及高性能计时器类,只要包含一下就可以。有的朋友会说log文件有多线程问题,计时器有多cpu问题。没错,但是我们程序是单线程的(见前面说明),所以写文件时候不加锁,另外尽量只建立一个全局log实例,保证创建、关闭文件操作非常少,而且也不进行内存new/delete操作。为何不使用OutputDebugString?好问题,这个函数可能导致一些时序上的问题(可以搜索codeproject上面有介绍文字),可以用,但是不如写文件那么simple。感觉这些也许是问题,但是还是尽量简化这些工具,够用就好。
经过性能数据收集,发现读取、解析文件使用时间不多,相比来说将数据序列化类实例比较耗时,因为我们加入了很多验证的函数调用,导致新的代码比前一个版本需要更多的调用。当然可以去掉这些验证,但是那我们还做这个新版本干嘛呢。
根据现在得到的数据,主要下面一些改进:一个是将一些集合类加入对map的继承关系,相当于重新又包装了一次抽象。原因是我们在查询这些集合类的时候都是使用的循环for-loop调用,相比map的查询,就差了不少。(学过数据结构的朋友应该了解stl的map查询性能要好很多,因为内部一般是用树状结构实现的)。另外一个改动就是把一些不必要的初始化操作放在后面进行,这样就导致我们需要在一些函数调用时加上它相关的初始化调用,代码复杂了不少,但是也没有办法。还有一个常用的手法就是使用cache,把一些运算结果缓存起来,下一次就直接拿出结果使用。cache最好在后期有了具体数据在进行,因为过早的优化,过早的加入cache机制不见得是正确的。为什么这么说呢?因为cache有一个可能失效甚至出错的可能,真正的结果可能因为某些数据变化而产生变化,如果这个时候cache系统不知道,那么就出错了,如果加入事件消息来刷新cache,也许就会让代码变得过于复杂,不容易维护和理解,也许性能还不如简单的版本快呢,就如同奥卡姆剃刀原理提到的那样,复杂的往往都是错的,或者是错误的根源。
总结说就是使用正确的数据结构,适当使用cache。如果这些还不行,那就得具体问题具体分析,后来我加入了更详细的log信息,发现用户端对于同一个文件做了多次的初始化调用,这也是优化时候需要注意的,客户端代码也需要相应的优化,也许它们的优化才是最关键的。虽然没有拿到客户端优化的结果,但我相信这个修改会比我前面做的任何改进都要显著,而且未必改动很复杂。
总的来说,这次代码改进不算是很成功,主要是一开始的方向不太对,尽管后来发现了真正的原因。我想主要问题是一开始就把目光聚集到非常微观的范畴里,导致大的问题没有得到解决。另外对于程序优化这件事,必须有实际的运行数据,猜测哪个地方可能会慢或者有问题,都是不客观的,在优化问题中,不应该出现“我觉得、我想”的字眼,什么地方慢什么地方需要改动,一定要用数据来说话。优化必然要对代码做这样那样的变动,这时候我们累积的test case就非常有用了,可以帮助我们确定代码改动是否引入了错误。
《“程序优化经验谈”》 有 1 条评论
看完了顺便消除一下0回复。