莊周夢蝶

          生活、程序、未來
             :: 首頁 ::  ::  :: 聚合  :: 管理

          Clojure的recur尾遞歸優(yōu)化探秘

          Posted on 2010-07-11 04:20 dennis 閱讀(4265) 評論(0)  編輯  收藏 所屬分類: javaClojure

              Clojure由于是基于JVM,同樣無法支持完全的尾遞歸優(yōu)化(TCO),這主要是Java的安全模型決定的,可以看看這個久遠的bug描述。但是Clojure和Scala一樣支持同一個函數(shù)的直接調(diào)用的尾遞歸優(yōu)化,也就是同一個函數(shù)在函數(shù)體的最后調(diào)用自身,會優(yōu)化成循環(huán)語句。讓我們看看這是怎么實現(xiàn)的。
              Clojure的recur的特殊形式(special form)就是用于支持這個優(yōu)化,讓我們看一個例子,經(jīng)典的求斐波那契數(shù):
          (defn recur-fibo [n]
               (letfn [(fib 
                          [current next n]
                          (
          if (zero? n)
                              current
                              ;recur將遞歸調(diào)用fib函數(shù)
                              (recur next (
          + current next) (dec n))))]
              (fib 
          0 1 n)))

              recur-fibo這個函數(shù)的內(nèi)部定義了一個fib函數(shù),fib函數(shù)的實現(xiàn)就是斐波那契數(shù)的定義,fib函數(shù)的三個參數(shù)分別是當前的斐波那契數(shù)(current)、下一個斐波那契數(shù)(next)、計數(shù)器(n),當計數(shù)器為0的時候返回當前的斐波那契數(shù)字,否則就將當前的斐波那契數(shù)設置為下一個,下一個斐波那契數(shù)字等于兩者之和,計數(shù)遞減并遞歸調(diào)用fib函數(shù)。注意,你這里不能直接調(diào)用(fib next (+ current next) (dec n)),否則仍將棧溢出。這跟Scala不同,Clojure是用recur關(guān)鍵字而非原函數(shù)名作TOC優(yōu)化。

              Clojure是利用asm 3.0作字節(jié)碼生成,觀察下recur-fibo生成的字節(jié)碼會發(fā)現(xiàn)它其實生成了兩個類,類似user$recur_fibo__4346$fib__4348和user$recur_fibo__4346,user是namespace,前一個是recur-fibo中的fib函數(shù)的實現(xiàn),后一個則是recur-fibo自身,這兩個類都繼承自 clojure.lang.AFunction類,值得一提的是前一個類是后一個類的內(nèi)部類,這跟函數(shù)定義相吻合。所有的用戶定義的函數(shù)都將繼承 clojure.lang.AFunction。
             
              在這兩個類中都有一個invoke方法,用于實際的方法執(zhí)行,讓我們看看內(nèi)部類fib的invoke方法(忽略了一些旁枝末節(jié))
           1 // access flags 1
           2   public invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; throws java/lang/Exception 
           3    L0
           4     LINENUMBER 2 L0
           5    L1
           6     LINENUMBER 4 L1
           7    L2
           8     LINENUMBER 4 L2
           9     ALOAD 3
          10     INVOKESTATIC clojure/lang/Numbers.isZero (Ljava/lang/Object;)Z

          11     IFEQ L3
          12     ALOAD 1
          13     GOTO L4

          14    L5
          15     POP
          16    L3
          17     ALOAD 2
          18    L6
          19     LINENUMBER 6 L6
          20     ALOAD 1
          21     ALOAD 2
          22     INVOKESTATIC clojure/lang/Numbers.add (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Number;

          23    L7
          24     LINENUMBER 6 L7
          25     ALOAD 3
          26     INVOKESTATIC clojure/lang/Numbers.dec (Ljava/lang/Object;)Ljava/lang/Number;

          27     ASTORE 3
          28     ASTORE 2
          29     ASTORE 1
          30     GOTO L0

          31    L4
          32    L8
          33     LOCALVARIABLE this Ljava/lang/Object; L0 L8 0
          34     LOCALVARIABLE current Ljava/lang/Object; L0 L8 1
          35     LOCALVARIABLE next Ljava/lang/Object; L0 L8 2
          36     LOCALVARIABLE n Ljava/lang/Object; L0 L8 3
          37     ARETURN
          38     MAXSTACK = 0
          39     MAXLOCALS = 0

              首先看方法簽名,invoke接收三個參數(shù),都是Object類型,對應fib函數(shù)里的current、next和n。
              關(guān)鍵指令都已經(jīng)加亮,9——11行,加載n這個參數(shù),利用Number.isZero判斷n是否為0,如果為0,將1彈入堆,否則彈入0。IFEQ比較棧頂是否為0,為0(也就是n不為0)就跳轉(zhuǎn)到L3,否則繼續(xù)執(zhí)行(n為0,加載參數(shù)1,也就是current,然后跳轉(zhuǎn)到L4,最后通過ARETURN返回值current作結(jié)果。
             
              指令20——22行,加載current和next,執(zhí)行相加操作,生成下一個斐波那契數(shù)。
              指令25-——26行,加載n并遞減。
              指令27——29行,將本次計算的結(jié)果存儲到local變量區(qū),覆蓋了原有的值。
              指令30行,跳轉(zhuǎn)到L0,重新開始執(zhí)行fib函數(shù),此時local變量區(qū)的參數(shù)值已經(jīng)是上一次執(zhí)行的結(jié)果。
             
              有的朋友可能要問,為什么加載current是用aload 1,而不是aload 0,處在0位置上的是什么?0位置上存儲的就是著名的this指針,invoke是實例方法,第一個參數(shù)一定是this。

             從上面的分析可以看到,recur干的事情就兩件:覆蓋原有的local變量,以及跳轉(zhuǎn)到函數(shù)開頭執(zhí)行循環(huán)操作,這就是所謂的軟尾遞歸優(yōu)化。這從RecurExp的實現(xiàn)也可以看出來:

             //覆蓋變量
            for (int i = loopLocals.count() - 1; i >= 0; i--) {
                          LocalBinding lb 
          = (LocalBinding) loopLocals.nth(i);
                          Class primc 
          = lb.getPrimitiveType();
                          
          if (primc != null) {
                              gen.visitVarInsn(Type.getType(primc).getOpcode(Opcodes.ISTORE), lb.idx);
                          }
                          
          else {
                              gen.visitVarInsn(OBJECT_TYPE.getOpcode(Opcodes.ISTORE), lb.idx);
                          }
                      }
             
          //執(zhí)行跳轉(zhuǎn)
             gen.goTo(loopLabel);
           
                recur分析完了,最后有興趣可以看下recur-fibo的invoke字節(jié)碼
           1  L0
           2     LINENUMBER 1 L0
           3     ACONST_NULL
           4     ASTORE 2
           5     NEW user$recur_fibo__4346$fib__4348
           6     DUP
           7     INVOKESPECIAL user$recur_fibo__4346$fib__4348.<init> ()V

           8     ASTORE 2
           9     ALOAD 2
          10     CHECKCAST user$recur_fibo__4346$fib__4348
          11     POP
          12    L1
          13    L2
          14     LINENUMBER 7 L2
          15     ALOAD 2
          16     CHECKCAST clojure/lang/IFn
          17     GETSTATIC user$recur_fibo__4346.const__2 : Ljava/lang/Object;
          18     GETSTATIC user$recur_fibo__4346.const__3 : Ljava/lang/Object;
          19     ALOAD 1
          20     ACONST_NULL
          21     ASTORE 1
          22     ACONST_NULL
          23     ASTORE 2
          24     INVOKEINTERFACE clojure/lang/IFn.invoke (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
          25    L3
          26     LOCALVARIABLE fib Ljava/lang/Object; L1 L3 2
          27    L4
          28     LOCALVARIABLE this Ljava/lang/Object; L0 L4 0
          29     LOCALVARIABLE n Ljava/lang/Object; L0 L4 1
          30     ARETURN

               5——7行,實例化一個內(nèi)部的fib函數(shù)。
               24行,調(diào)用fib對象的invoke方法,傳入3個初始參數(shù)。

               簡單來說,recur-fibo生成的對象里只是new了一個fib生成的對象,然后調(diào)用它的invoke方法,這也揭示了Clojure的內(nèi)部函數(shù)的實現(xiàn)機制。
                

          主站蜘蛛池模板: 万宁市| 襄汾县| 大同县| 龙州县| 融水| 灵山县| 临潭县| 沙河市| 阜南县| 济宁市| 正安县| 岳阳市| 醴陵市| 虞城县| 华容县| 犍为县| 土默特左旗| 平昌县| 青浦区| 新河县| 永顺县| 灵石县| 仲巴县| 嘉荫县| 沙田区| 松滋市| 张家口市| 华坪县| 吕梁市| 三门县| 揭西县| 旌德县| 剑川县| 安泽县| 寿宁县| 福州市| 仙居县| 嘉兴市| 萍乡市| 嘉定区| 龙江县|