Lua数据结构 — 闭包(四)
作者:罗日健 前面几篇文章已经说明了Lua里面很常用的几个数据结构,这次要分享的也是常用的数据结构之一 –?函数的结构。函数在Lua里也是一种变量,但是它却很特殊,能存储执行语句和被执行,本章主要描述Lua是怎么实现这种函数的。 在脚本世界里,相信闭包这个词大家也不陌生,闭包是由函数与其相关引用环境组成的实体。可能有点抽象,下面详细说明: 一、 闭包的组成
闭包主要由以下2个元素组成:
不难发现,Lua的闭包分成2类,一类是CClosure,即luaC函数的闭包。另一类是LClosure,是Lua里面原生的函数的闭包。下面先讨论2者都有相同部分ClosureHeader:
对于CClosure数据结构:
对于LClosure数据结构:
? 二、 闭包的UpVal实现究竟什么是UpVal呢?先来看看代码:
分析一下上面这段代码,最终testB的值显然是3+5+10=18。当调用testA(5)的时候,其实是在调用FuncB(5),但是这个FuncB知道a = 3,这个是由FuncA调用时,记录到FuncB的外部变量,我们把a和c称为FuncB的upvalue。那么Lua是如何实现upvalue的呢? 以上面这段代码为例,从虚拟机的角度去分析实现流程: 1) FuncA(3)执行流程
虚拟机操作:(帮助理解,与真实值有差别) LOADK top 3 //把3这个常量放到栈顶 CALL top FuncA nresults //调用对应的FuncA函数
虚拟机操作: LOADK top 10 //local c = 10
上面生成一个闭包之后,因为在Lua里,函数也是一个变量,上面的语句等价于local FuncB = function() … end,所以也会生成一个临时的FuncB到栈顶。
虚拟机操作:
虚拟机操作: 2) FuncB的执行过程到了FuncB执行的时候,参数b=5已经放到栈顶,然后执行FuncB。语句比较简单和容易理解,return a+b+c 虚拟机操作如下: 到这里UpVal的创建和使用也在上面给出事例说明,总结一下UpVal的实现:
lua code:
? 三、 函数原型之前说的,函数原型是表明一段可执行的代码或者操作指令。在绑定到Lua空间的C函数,函数原型就是lua_CFunction的一个函数指针,指向用户绑定的C函数。下面描述一下Lua中的原生函数的函数原型,即Proto数据结构(lobject.h 231-253): 引用内容:
Proto的所有参数都是在语法分析和中间代码生成时获取的,相当于编译出来的汇编码一样是不会变的,动态性是在Closure中体现的。 ? 四、 闭包运行环境在前面说到的闭包数据结构中,有一个成员env,是一个Table*指针,用于指向当前闭包运行环境的Table。 什么是闭包运行环境呢?以下面代码举例: 上面代码中的d = 20,其实就是在环境变量中取env[“d”],所以env一定是个table,而当定义了本地变量之后,之后的所有变量都对从本地变量中操作。 ? 五、 函数调用信息函数调用相当于一个状态信息,每次函数调用都会生成一个状态,比如递归调用,则会有一个栈去记录每个函数调用状态信息,比如说下面这段没有意义的代码: 那么每次调用将会生成一个调用状态信息,上面代码会无限生成下去:
究竟一个CallInfo要记录哪些状态信息呢?下面来看看CallInfo的数据结构:
? 六、 函数调用的栈操作上面描述的CallInfo信息,具体整个流程是怎么走的,结合下面代码详细地叙述整个调用过程,栈是怎么变化的: 假设现在走到了funcA(30,40)这个语句,在执行前已经存在了global这个闭包和funcA这个闭包,在调用global这个闭包时,已经生成了一个global的CallInfo。 1) 函数调用的栈操作:(OP_CALL lvm.c 582-601)
当前虚拟机的pc指针,指向global函数原型中的CALL指令,这时global的CallInfo的savedpc就会保存当前pc。然后会把要执行的funcA的闭包放到栈顶。 – 参数分别放到栈顶(从左到右分别进栈),生成funcA的CallInfo,并把完成对应CallInfo栈操作
2) 函数返回的栈操作:(OP_RETURN lvm.c 635-648)
? 七、 尾调用(TAILCALL)
尾调用是一种对函数解释的优化方法,对于上面代码,改造成下面代码后,则不会出现stack overflow: 上面的Recursion方法不会出现stack overflow错误,也能顺利算出Recursion(20000) = 200010000。尾调用的使用方法十分简单,就是在return后直接调用函数,不能有其它操作,这样的写法即会进入尾调用方式。 那究竟lua是如何实现这种尾调用优化的呢?尾调用是在编译时分析出来的,有独立的操作码OP_TAILCALL,在虚拟机中的执行代码在lvm.c 603-634,具体原理如下: 1)首先像普通调用一样,准备调用Recursion函数
2)关闭Recursion1的调用状态,把Recursion2的对应栈数据下移,然后重新执行
本质优化思想:先关闭前一个函数,销毁CallInfo,再调用新的CallInfo,这样就会避免全局CallInfo栈溢出。 ? 八、 总结本文讨论了闭包、UpVal、函数原型、环境、栈操作、尾调用等相关知识,基本上把大部分的知识点和细节也囊括了,另外还有2大块知识:函数原型的生成和闭包GC可能迟些再分享。 来自 :http://blog.aliyun.com/845?spm=0.0.0.0.JjfWaQ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |