Clojure的recur尾递归优化探秘

简介:
    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的内部函数的实现机制。
   文章转自庄周梦蝶  ,原文发布时间 2010-07-11
相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
1月前
|
Java 调度 Android开发
构建高效Android应用:探究Kotlin多线程编程
【2月更文挑战第17天】 在现代移动开发领域,性能优化一直是开发者关注的焦点。特别是在Android平台上,合理利用多线程技术可以显著提升应用程序的响应性和用户体验。本文将深入探讨使用Kotlin进行Android多线程编程的策略与实践,旨在为开发者提供系统化的解决方案和性能提升技巧。我们将从基础概念入手,逐步介绍高级特性,并通过实际案例分析如何有效利用Kotlin协程、线程池以及异步任务处理机制来构建一个更加高效的Android应用。
35 4
|
15天前
|
存储 算法 编译器
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)
|
6天前
|
Java API Android开发
构建高效Android应用:探究Kotlin协程的优化实践
【4月更文挑战第13天】 随着移动应用开发的不断进步,对性能和用户体验的要求日益增高。在众多提升应用性能的手段中,异步编程技术尤为关键。Kotlin协程作为一种新兴的异步处理方式,因其轻量级线程管理和简洁的API设计,成为Android开发中备受青睐的技术。本文将深入探讨Kotlin协程在Android中的应用实践,重点分析其如何优化后台任务处理,提升应用响应速度,并确保用户界面流畅性。通过实例演示和代码分析,揭示协程在现代Android开发中的重要作用及其实现细节。
|
15天前
|
算法 搜索推荐 Serverless
掌握Go语言:Go语言递归函数,解密编程之谜,探索算法的奥秘!(27)
掌握Go语言:Go语言递归函数,解密编程之谜,探索算法的奥秘!(27)
|
1月前
|
前端开发 编译器
编译原理 - 编译优化
编译原理 - 编译优化
19 0
|
2月前
|
缓存 Rust 算法
Rust中的数据结构与算法优化实践
在Rust编程语言中,优化数据结构与算法是提高程序性能的关键。本文首先介绍了Rust的特点,然后重点讨论了如何在Rust中优化数据结构和算法,包括使用标准库中的高效数据结构、自定义数据结构的优化技巧、算法选择与改进、以及Rust特性如所有权和借用检查器的应用。通过实际案例,我们将展示如何在Rust中实现更高效的数据结构与算法。
|
2月前
|
开发者 Python
深入浅出Python协程:提高并发编程效率
在现代软件开发中,提高程序的执行效率和响应速度是一个永恸的追求。本文将深入探讨Python协程(Coroutine)的原理及其在并发编程中的应用。与传统的多线程和多进程相比,协程提供了一种更轻量级、成本更低的并发执行方案。我们将通过实例详细说明如何使用Python的asyncio库来编写协程,解析其背后的事件循环机制,并探讨如何通过协程提高程序的并发处理能力,最终达到提高程序执行效率的目的。本文旨在为读者揭示协程技术的魅力,帮助读者掌握使用Python进行高效并发编程的技巧。
12 0
|
监控 前端开发 Java
JIT优化之道
《JIT优化之道》是去年在公司的一次分享,对于公司组织分享我是赞同又不赞同,怎么讲呢? 技术分享当然是好的,这是一个双赢,分享者教学相长,而收听者也能更快的了解进步。 但以前在原先的公司也做过些类事情,但没有想象的好,大家对分享主题的探索也只限于在分享时间段内,过后很少有人,几乎没人去做进一步的探索。填鸭式的学习效果甚微。后来只涉及一些项目中使用到的知识点,让项目中人去发现项目中的一些亮点,盲区 聪明人从旁人的错误中吸取教训,愚笨人则从自身的错误中吸取教训,有多少聪明人呢?不经历风雨又怎么见彩虹?
267 0
JIT优化之道
|
算法 Java 测试技术
【Java数据结构及算法实战】系列004:程序性能的两种确定方式
本节是《Java数据结构及算法实战》系列的第4节,主要介绍程确定序性能的两种方式。
89 0
【Java数据结构及算法实战】系列004:程序性能的两种确定方式
|
缓存 算法 NoSQL
【Java数据结构及算法实战】系列003:程序性能的两种表示方式
本节是《Java数据结构及算法实战》系列的第3节,主要介绍程序性能的两种表示方式。 评价一个程序好坏的指标非常多,比如易用性、稳定性、可维护性等等,但一个最为重要的评价指标是性能。性能是其他评价指标的基础。 比如,在Web网站响应时间方面,业界的评判标准是主样的: 在2秒之内给客户响应被用户认为是“非常有吸引力”的用户体验。 在5秒之内给客户响应被用户认为是“比较不错”的用户体验。 在10秒之内给客户响应被用户认为是“糟糕”的用户体验。 如果超过10秒还没有得到响应,那么大多数用户会认为这次请求是失败的。 由此我们可以得出结论,Web网站的性能越好,处理时间越短,响应时间越短,则会有更好
119 0