夜隼

RYSZ

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

性能误区

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://id-11546.blogbus.com/logs/168696.html

我曾经领导开发的一个项目发生过的一件事,给我留下了很深的印象——

在开发并发布了第一个版本之后,经过一段时间收集来自于客户和维护人员反馈的信息,我们决定重新设计一个全新的2.0版本。2.0版本需要解决两个问题:新功能和性能。功能是在设计结束后就确定是否具备的,但性能却只能通过测试才能够得到证实。经过精心的设计之后,我们确定新的设计一定能够大幅度的提高性能。但开发初步完成之后,测试的结果让我大吃一惊,性能是提高了,但远比我们预计的要差的多。经过使用相关的工具进行测量,最后的原因更是让我大跌眼镜。一段非常频繁的被调用的代码中有一段这样的代码:

void do_something(void)
{
   for(unsigned int i=0; i < get_total_number(); i++){
          ... do some other things ...
   }

}

由于get_total_number函数其内部实现相当复杂,并且其返回值的数值可能会非常庞大,所以,这个函数的内部复杂过程会被非常频繁的执行,无谓的损耗的大量性能。

进行简单的修改之后,代码变为:

void do_something(void)
{
   unsigned int number = get_total_number();
   for(unsigned int i=0; i < number; i++){
          ... do some other things ...
   }
}

再次测试,性能成倍的提高,最终的结果比我们预计的性能还要好!

当我把这件事情告诉一个同为程序员的朋友时,他只说了一句话:“这是基本的编程素养问题”。我当时从心里对此表示赞同。因为从我刚开始会编程时,就有前辈给我了一个忠告——基于性能的考虑,如果一个函数的返回值可能被多次用到,即使这个函数只是一个查询函数——即只要参数给定(如果有的话),每次调用其返回值总是相同的——也最好使用一个临时变量来保存它,以避免由于多次函数调用带来的性能损失。一直以来,我对这一点奉若圣经,并经常用它来指导别人编程。

最近看了一本即平常又很棒的书——《重构》——让我重新对这个问题进行了思考。

书中所讲述的重构方法中,有一个被称作Replace Temp with Query的。它建议,在重构过程中,尽量减少原有代码中的临时变量。如果一个临时变量在第一次被赋值后,以后只是读取保存在这个临时变量中的值,而并不修改它,则应该把所有使用这个临时变量的地方替换为使用查询函数。比如:

class foo{
  public:
  .......
  unsigned int func(unsigned int __price){
        unsigned int __consumed = get_num()*__price;
        unsigned int __rv;
        for(unsigned int i=0; i < count; i++){
           __rv += __consumed * i;
        }
        cout << "consumed:" << __consumed << endl;
        return __rv;
     }
};

代码中的变量__consumed应该被替换为查询函数,重构后的代码如下:

class foo{
  public:
  .......
  unsigned int func(unsigned int __price){
        unsigned int __rv;
        for(unsigned int i=0; i < count; i++){
           __rv += get_consumed(__price) * i;
        }
        cout << "consumed:" << get_consumed(__price) << endl;
        return __rv;
  }
  unsigned int get_consumed(unsigned int __price)
  {
     return get_num()*__price;
  }
};

当我第一次看到这一点时,我马上想起之前的事情:这样做会影响性能!!

是的,Replace Temp with Query肯定会影响性能,但正如你看到的,它也能让代码更清晰,更利于维护。一边是性能,一边是可维护性,耳边飘起了张学友的《左右为难》...

 

性能重要吗?对所有的程序员,IT Manager,以及客户来说,这是一个答案非常明确的问题。没有人不希望自己编写,销售或使用的系统更快。曾经在SPM的位置上,对这一点更加有切肤之痛。

可维护性重要吗?既然这是一个优点,我相信每个人也会给出肯定的答案,但我也同样相信,并非每个人在实际的工作中都真正重视了这一点。即使非常有经验的程序员也会经常在别的诱惑面前舍弃它。于是,我们经常能够看到所谓的高性能的天书般的代码。

经验及研究表明,如果不进行重构,由于需求变更,设计变更,排除bug等原因,所有代码经过一定次数的修改后,都会开始腐烂变质。其特征为,越来越难以阅读与理解,一些架构和逻辑变得越来越不合理,BUG数量越来越多却越来越难以排除和修复,等等等等。就像所有的生命体一样,逐渐衰老,最终走向死亡。

重构是解决这个问题的良药。同时,重构和可维护性是密切相关,相辅相成的。可维护性有利于重构的进行,而重构的一个重要目标就是可维护性。所以,可维护性对于防止及延缓程序的衰老是非常重要的。每个真正的程序员都要让“编写可维护性代码”成为一条纪律,一个习惯。

那么,性能呢?难道为了可维护性,就要放弃性能吗?如果给客户提供一个其性能无法忍受的系统,客户一定会拒绝使用它。

但问题是,我们在之前的例子中,使用Replace Temp with Query所带来的性能损失是否真的很明显?作者对这个问题的态度是,先重构它,至于性能,那是优化阶段的事情。所谓优化,是根据测试的结果,去修改那些对性能影响明显的地方以提高性能。

根据经验得来的局部性原理,一个系统“90%的时间运行在10%的代码处”。如果性能和可维护性之间产生冲突的时候(两者并非总是矛盾的),我们需要通过测试的结果来决定取舍。对于一段代码,如果你通过牺牲其可维护性,而换来微不足道的性能提升,用一句谚语来形容——“丢了西瓜,捡了芝麻”。但如果一段代码,优化后可以非常显著的提高性能,并且优化的途径只能是牺牲可维护性,还有一个前提,如果这种性能优化有利于提高你产品的竞争力;那么,不要犹豫,去做就是了,就像我在我们的产品中做的那样。

为什么一定要有这个前提,即只有当你所做的性能优化有利于提高产品的竞争力时,你才有必要去做性能优化。想一下,假如你的系统已经足够快了,客户对这种性能已经感到非常满意,那么你还有必要通过牺牲可维护性去进一步提高性能吗?当然没有!因为差的可维护性可能导致更多的错误,而一个飞快却会崩溃的程序,远没有一个足够快却非常稳定的系统对客户更有价值。

posted on 2009-02-27 10:23  夜隼  阅读(248)  评论(0编辑  收藏  举报