浅谈代码的执行效率(2):编译器的威力

在上一篇文章中,我主要表达了这样一个观点:影响程序效率的关键之一是算法,而算法的选择与优化,和是否多一个赋值少一个判断的关系不大。关于算法的选择,我谈到其理论上的复杂度,并不直接反映出效率。因为在实际运用时,数据的规模,特征等等都会涉及到算法的实际效果。一个时间复杂度低的算法并不代表任何情况下的效率都高。这是“实际”和“理论”的区别之一。现在我打算来谈一下另一个比较“实际”的东西:编译器对于程序效率的影响。

那么我们先来看这样一段代码,假设有一个保存着整数的单向链表,要求您写一个函数进行求和,您会怎么写呢?如果我们用F#,那么最容易实现的自然是递归方式:

let rec sum ls =
   match ls with
   | [] -> 0
   | x :: xs -> x + (sum xs)

这段F#代码使用了模式匹配:如果是一个空链表,那么其结果自然等于零,否则就把它第一个元素加上剩余元素之和。这段代码显然非常简单,通过声明式的方法“表达”了函数的逻辑,没有任何一句多余的代码。不过您一定发现了,这个函数的实现中使用了递归,因此对于长度为n的链表来说,这个函数的时间复杂度为O(n),空间复杂度也是O(n)——空间的开销在函数的调用栈上。一般来说,递归总是难逃调用栈的累积,因此它的空间复杂度总是难以做到O(1)的常数级别。调用栈的堆积,使得程序在执行访问的内存地址跨度不断增大,容易导致程序的局部性(Locality)不佳,性能较差——关于代码局部性的影响,我们下篇文章中再进行讨论。

当然,有些朋友可能会说,为什么要用递归呢?用普通的一个for或while循环,然后不断累加不也可以吗?当然可以,而且这么做的话空间复杂度便是O(1)了,时间复杂度虽然还是O(n),但是经过上面的描述,我们可以知道它的实际执行效率会比递归的方式要好。不过,for/while都是命令式编程的方式,不适合函数式编程的“声明”风格,因此在实际应用中我们往往会写这样的sum函数:

let sum ls =
   let rec sum' ls acc =
     match ls with
     | [] -> acc
     | x :: xs -> sum' xs (acc + x)

   sum' ls 0

这个sum函数中定义了一个辅助函数sum',这个辅助函数会多一个参数作为“累加器”,其中依旧使用了模式匹配的语法,在遇到空数组时直接返回累加器的值,否则就把链表的第一个元素加至累加器中,再递归调用sum'辅助函数。没错,这里还是用了递归。这样看起来,它的执行效果应该和前一种实现没有多大差别?

那么实际结果又是如何呢?如果您有条件不妨可以自己尝试一下——我这里贴一下由.NET Reflector反编译为C#后的sum'函数代码:

[Serializable] 
internal class sum'@9 : OptimizedClosures.FSharpFunc<FSharpList<int>, int, int>
{
   public override int Invoke(FSharpList<int> ls, int acc)
   {
     while (true)
     {
       FSharpList<int> list = ls;
       if (!(list is FSharpList<int>._Cons))
       {
         return acc;
       }
       FSharpList<int>._Cons cons = (FSharpList<int>._Cons)list;
       FSharpList<int> xs = cons.get_Tail();
       int x = cons.get_Head();
       acc += x;
       ls = xs;
     }
   }
}

时间: 2024-12-02 03:20:54

浅谈代码的执行效率(2):编译器的威力的相关文章

浅谈代码的执行效率(3):缓存与局部性

在前两篇文章里,我们讨论了程序性能的两个方面,一是算法(广义的算法,即解决问题的方法),二是编译器.通过这两个方面,我想表达的意思是,一段程序的执行效率,是很难从表面现象得出结论的,至少从一些简单的层面,如代码的长度是几乎难以说明任何问题--因此一定要进行Profiling才能做到有效的优化.而现在,我们假设两段程序算法基本相同,编译器也只是进行简单的"翻译",那么--我们能从"表面"看出性能高下吗? 那么就从一个最简单的例子看起吧.假设DoSomethingA和D

浅谈代码的执行效率(4):汇编优化

终于谈到这个话题了,首先声明我不是汇编优化的高手,甚至于我知道的所有关于汇编优化的内容,仅仅来自于学校的课程.书本及当年做过的一些简单练习.换句话说,我了解的东西只能算是一些原则,甚至也有一些"陈旧"了--不过我想既然是一些原则性的东西,还是能够用它来做一定程度的判断.至少我认为,我在博客园里看到的许多关于"汇编优化"也好,"内嵌汇编"也罢的说法,经常是有些问题的. 说到汇编优化,自然被人想到"高性能".似乎用.NET或Jav

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

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

浅谈如何提高编程效率?

浅谈如何提高编程效率?1.提高工作经验 经验来自实践.平时多阅读一些技能方面的书籍和来自各网站上的优秀文章.如果说,一本书就是一个台阶,那么在人的一生中将有千万道台阶等着我去跨越.每跨越一步台阶,将得到不可估量的财富,而下一步台阶,又将带我步入一个新的境界,获取新的知识. 看到学到做到.平时有时间多看看大牛写的代码,多看看开源的项目并参与一些开源项目的编码工作.2.和大牛.勤奋的人一起工作 和大牛有经验的程序猿一起工作.和勤奋的人一起共事.永远不要相信"你改变不了环境,但可以改变自已."

浅谈onTouch先执行,还是onClick执行(详解)

有一个Button 按钮,要想为该按钮设置onClick事件和OnTouch事件 mTestButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.d(TAG, "onClick execute"); } }); mTestButton.setOnTouchListener(new View.OnTouchListener() { @Ove

浅谈提升C#正则表达式效率

说到C#的Regex,谈到最多的应该就是RegexOptions.Compiled这个东西,传说中在匹配速度方面,RegexOptions.Compiled是可以提升匹配速度的,但在启动速度上,使用了RegexOptions.Compiled情况下,通常会使启动速度慢许多,据说最多是60倍. 进行一组测试,有测试数据,才有讨论依据. 第一步,帖上测试硬件信息(呵呵,硬件有点烂:() 第二步, a.测试在没有使用RegexOptions.Compiled项时候的情况,随意使用一些内容,然后循环一万

一起谈.NET技术,浅谈提升C#正则表达式效率

说到C#的Regex,谈到最多的应该就是RegexOptions.Compiled这个东西,传说中在匹配速度方面,RegexOptions.Compiled是可以提升匹配速度的,但在启动速度上,使用了RegexOptions.Compiled情况下,通常会使启动速度慢许多,据说最多是60倍. 进行一组测试,有测试数据,才有讨论依据. 第一步,帖上测试硬件信息(呵呵,硬件有点烂:() 第二步, a.测试在没有使用RegexOptions.Compiled项时候的情况,随意使用一些内容,然后循环一万

浅谈javascript中执行环境(作用域)与作用域链_javascript技巧

相信很多初学者对与javascript中的执行环境与作用域链不能很好的理解,这里,我会按照自己的理解同大家一起分享. 一般情况下,我们把执行环境分为全局执行环境和局部执行环境,其中局部执行环境我们又可以称之为函数执行环境.那么究竟什么使执行环境呢?通俗的说,执行环境即为代码执行时所处的环境.我们下来看一看如下代码,再进一步分析之. <script><br>var name="zhuzhenwei"; function changeName(){ if (name

浅谈站长心态及效率

分三部分1.当前网站形势 2.提高效率降低成本 3.心态决定成长,适者生存 一.当前网站形势大家都是搞网站的,深知做网站的难处 因为眼下网站收益越来越难,多数站长都是靠联盟吃饭的.网络管理逐步严格,广告联盟也不景气了.且正趋向于"人人都有网站年代"的到来. 目前,每天都会有新的网站成立,同样,也有很多的网站倒下,增长的个数总是比倒下去的多,就跟中国人口一样,照这样下去,我想大家不 难想像的到吧 另外网站多了,推广就会难,流量就会少.网络上流传的各种各样的宣传网站的方式,我想很多站长都已