浅谈代码的执行效率(一)

最新做项目,我们的项目主管看了我写的代码后,觉得代码执行效率不是很好,在这个方面需要加以改进,自己对代码的执行效率了解比较少,所以就上园子里搜了下,博客园里面高手多的是,呵呵,向他们学习应该是个比较不错的途径。下面就是技术大牛赵劼赵老师对代码的执行效率的独到见解(呵呵,慢慢看,看一篇,我就先在这里粘贴一篇。)感谢有这样的技术先辈们,提供给我们可以向他们学习的机会。为了保持原文的语义不变,我就复制粘贴(当然,还是需要句句斟酌的。),以免由于自己的不当筛选造成不必要的误解。

 

                                         浅谈代码的执行效率(1) 算法是关键

 

前一段时间在博客园里看到这样一篇文章,那位兄弟谈到程序效率的关键是“简短”。他说,“程序越简短,其可执行代码就越少,就越有效率”,而在编写程序的时候,“要尽量改进我们的算法,而改进算法中最重要的一条,就是减少语句”。这句话从表面上似乎正确,但我认为性能这问题不能用“简短”这种方式去思考,否则会进入一些误区。我整理了一下思路,希望可以从几个方面把详细谈一下这个问题。

首先,如果说“简短的代码效率高”,那么什么叫作“简短”呢?姑且理解为“代码少”吧。如果“代码少”能够得出“效率高”,那我认为还需要其他一些条件,例如:

  • 代码完全是顺序执行的
  • 每行代码的效率相同

但是,这对于使用高级语言的程序员来说,这两条基本上都无法成立,因为:

  • 代码中有条件跳转(如循环),因此一段简短的代码最终执行的“次数”可能比一段复杂的代码要多。这是很常见的情况,并非如那位兄弟所说“两三行代码写出死循环”这样的“特例”。
  • 每行代码的效率几乎不可能相同。试想一个i++和一个方法调用,他们的性能开销怎么可能相同呢?再者,这个特性并非是“高级语言”特有的,连CPU指令也是一样(以后我们再来详谈这个问题)。

其实这就是目前编程工作中不可能回避的现状,那就是高级语言并不直接反映出机器的执行过程。同时,我们不能从代码表面的简短程度来判断程序效率的高低。正如那位朋友所谈,影响程序的关键因素之一是算法的选择,但我不同意他说算法优化的关键在于“减少语句”。从一定程度上讲,选择高效的算法的确是为了减少指令执行的“总量”,但它并不是如那篇文章所说通过“少一些变量”,“少一些判断”来进行的,而是在于大幅的逻辑改变来减少总体的操作。

事实上,我们很容易便能举出代码简短,但是性能较差的算法。就拿最常见的排序算法来说,冒泡排序不可谓不简单:

 

ExpandedBlockStart.gif 代码
 1  public   void  BubbleSort( int [] array)
 2  {
 3       for  ( int  i  =  array.Length  -   1 ; i  >=   0 ; i -- )
 4      {
 5           for  ( int  j  =   1 ; j  <=  i; j ++ )
 6          {
 7               if  (array[j  -   1 >  array[j])
 8              {
 9                   int  temp  =  array[j  -   1 ];
10                  array[j  -   1 =  array[j];
11                  array[j]  =  temp;
12              }
13          }
14      }
15  }

 

而快速排序与之相比实在是复杂太多了:

 

ExpandedBlockStart.gif 代码
 1  public   void  QuickSort( int [] array)
 2  {
 3      QuickSortInternal(array,  0 , array.Length);
 4  }
 5 
 6  private   void  QuickSortInternal( int [] array,  int  begin,  int  end)
 7  {
 8       if  (end  ==  begin)
 9      {
10           return ;
11      }
12       else
13      {
14           int  pivot  =  FindPivotIndex(array, begin, end);
15           if  (pivot  >  begin) QuickSortInternal(array, begin, pivot  -   1 );
16           if  (pivot  <  end) QuickSortInternal(array, pivot  +   1 , end);
17      }
18  }
19 
20  private   int  FindPivotIndex( int [] array,  int  begin,  int  end)
21  {
22       int  pivot  =  begin;
23       int  m  =  begin  +   1 ;
24       int  n  =  end;
25 
26       while  (m  <  end  &&  array[pivot]  >=  array[m])
27      {
28          m ++ ;
29      }
30 
31       while  (n  >  begin  &&  array[pivot]  <=  array[n])
32      {
33          n -- ;
34      }
35 
36       while  (m  <  n)
37      {
38           int  temp  =  array[m];
39          array[m]  =  array[n];
40          array[n]  =  temp;
41 
42           while  (m  <  end  &&  array[pivot]  >=  array[m])
43          {
44              m ++ ;
45          }
46 
47           while  (n  >  begin  &&  array[pivot]  <=  array[n])
48          {
49              n -- ;
50          }
51 
52      }
53 
54       if  (pivot  !=  n)
55      {
56           int  temp  =  array[n];
57          array[n]  =  array[pivot];
58          array[pivot]  =  temp;
59 
60      }
61 
62       return  n;
63  }

 

 

OMG,为什么快速排序会那么复杂?其原因在于,快速排序的性能关键之一,在于选择一个好的中轴(pivot)元素,选择一个好的中轴元素可以尽可能减少的交换操作的次数,以此提高算法的效率。而冒泡排序,做法的确“简短”,但是其性能在大部分场合都不如快速排序来的好。而且,同样是快速排序,也可以有多种实现方法,有的语言还可以实现地非常简单,如Haskell:

 

 

1  qsort []  =  []
2  qsort (x:xs)  =  qsort (filter ( <  x) xs)  ++  [x]  ++  qsort (filter ( >=  x) xs)

 

 

这便是Haskell版的快速排序,够短吧。但是它的效率不如之前那段长长的C#——当然,这么比可能不公平,因为两者使用的数据结构不同,C#基于数组进行排序,而Haskell却使用的是不可变的单向链表。

算法是效率的关键,但选择合适的算法也并非是一件十分容易的事情。我们都知道一个算法它有时间复杂度,空间复杂度等等,但这些是什么呢?是从数学角度得出的“理论值”。一个算法的时间复杂度,考虑的是算法的时间随规模增长的变化规律,它能确保的只是“当规模大到一定程度时”,时间复杂度低的算法效率肯定相对较高。但是在实际开发过程中,我们遇到的数据量往往都是有限的,其形式甚至还有一定规律可循。因此,“理论上”时间复杂度低的算法,并非时时刻刻都有上佳表现。

例如,对于一个已经排序的数组来说,冒泡排序的性能无人能敌;对于一个高效的快速排序算法来说,如果元素数量少于一个阈值时就会使用插入排序(因为快速排序时间复杂度的常数较大);再比如,一个算法使用空间换时间,但是空间一大,物理内存中的数据可能就会被放到磁盘交换区上(也有人把它叫做虚拟内存,但是我不认同这个叫法),此时读取时可能就要从硬盘上重新加载数据页,性能可能就更低一些了。说个更近的,有时候每次都创建对象,性能反而比缓存起来要高,不是吗?

因此我认为,无论怎么样,“简短”不应该是评价一段代码是否高效的因素。您可能会说,算法对性能来说自然很关键,但是这里说的“简短”不是指算法的改变,而是指在算法相同的情况下,少一些赋值,判断操作等等,这样指令少了性能不是更高了吗?不过我的考虑是,既然算法已经确定了,那么究竟有哪些地方值得我们减少一些赋值或判断操作呢?即便这样的确有效果,但我想,如果保留合理的赋值和判断如果可以让代码更清晰,那就不要进行如此“手动”而“原始”更“想当然”的优化吧。清晰、正确比高效更重要。

最重要的是,没有Profiling,一切优化都是徒劳的。如果您认为减少赋值和判断可以有效提高性能,那么也用Profiling来证明一下,不是吗?当然,我并没有说一些细节上的调整对性能影响不大,我接下来也会讨论直接对汇编进行的优化。但是,即使我们有需要优化汇编的场景……它们基本上也不是靠所谓减少赋值和判断来起到效果的。

最后补充一句:这篇文章里我谈到的“算法”是指广义的“实现方式”,通俗地讲便是“代码的写法”。而“冒泡排序”等面向指定问题的解法,我把它称之为“经典算法”。在这里,我们还是加以区分吧。

 

原文链接:http://www.cnblogs.com/JeffreyZhao/archive/2010/01/07/short-code-is-not-always-fast-1-algorithms.html

再次感谢赵老师!

转载于:https://www.cnblogs.com/angleSJW/archive/2010/03/22/1691640.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值