Clojure由于是基于JVM,同樣無法支持完全的尾遞歸優化(TCO),這主要是Java的安全模型決定的,可以看看這個
久遠的bug描述。但是Clojure和Scala一樣支持同一個函數的直接調用的尾遞歸優化,也就是同一個函數在函數體的最后調用自身,會優化成循環語句。讓我們看看這是怎么實現的。
Clojure的recur的特殊形式(special form)就是用于支持這個優化,讓我們看一個例子,經典的求斐波那契數:
(defn recur-fibo [n]
(letfn [(fib
[current next n]
(if (zero? n)
current
;recur將遞歸調用fib函數
(recur next (+ current next) (dec n))))]
(fib 0 1 n)))
recur-fibo這個函數的內部定義了一個fib函數,fib函數的實現就是斐波那契數的定義,fib函數的三個參數分別是當前的斐波那契數(current)、下一個斐波那契數(next)、計數器(n),當計數器為0的時候返回當前的斐波那契數字,否則就將當前的斐波那契數設置為下一個,下一個斐波那契數字等于兩者之和,計數遞減并遞歸調用fib函數。注意,你這里不能直接調用(fib
next (+ current next) (dec n)),否則仍將棧溢出。這跟Scala不同,Clojure是用recur關鍵字而非原函數名作TOC優化。
Clojure是利用asm 3.0作字節碼生成,觀察下recur-fibo生成的字節碼會發現它其實生成了兩個類,類似user$recur_fibo__4346$fib__4348和user$recur_fibo__4346,user是namespace,前一個是recur-fibo中的fib函數的實現,后一個則是recur-fibo自身,這兩個類都繼承自 clojure.lang.AFunction類,值得一提的是前一個類是后一個類的內部類,這跟函數定義相吻合。所有的用戶定義的函數都將繼承 clojure.lang.AFunction。
在這兩個類中都有一個invoke方法,用于實際的方法執行,讓我們看看內部類fib的invoke方法(忽略了一些旁枝末節)
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接收三個參數,都是Object類型,對應fib函數里的current、next和n。
關鍵指令都已經加亮,9——11行,加載n這個參數,利用Number.isZero判斷n是否為0,如果為0,將1彈入堆,否則彈入0。IFEQ比較棧頂是否為0,為0(也就是n不為0)就跳轉到L3,否則繼續執行(n為0,加載參數1,也就是current,然后跳轉到L4,最后通過ARETURN返回值current作結果。
指令20——22行,加載current和next,執行相加操作,生成下一個斐波那契數。
指令25-——26行,加載n并遞減。
指令27——29行,將本次計算的結果存儲到local變量區,覆蓋了原有的值。
指令30行,跳轉到L0,重新開始執行fib函數,此時local變量區的參數值已經是上一次執行的結果。
有的朋友可能要問,為什么加載current是用aload 1,而不是aload 0,處在0位置上的是什么?0位置上存儲的就是著名的this指針,invoke是實例方法,第一個參數一定是this。
從上面的分析可以看到,recur干的事情就兩件:覆蓋原有的local變量,以及跳轉到函數開頭執行循環操作,這就是所謂的軟尾遞歸優化。這從RecurExp的實現也可以看出來:
//覆蓋變量
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);
}
}
//執行跳轉
gen.goTo(loopLabel);
recur分析完了,最后有興趣可以看下recur-fibo的invoke字節碼
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行,實例化一個內部的fib函數。
24行,調用fib對象的invoke方法,傳入3個初始參數。
簡單來說,recur-fibo生成的對象里只是new了一個fib生成的對象,然后調用它的invoke方法,這也揭示了Clojure的內部函數的實現機制。