在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
作者:罗日健 前面几篇文章已经说明了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可能迟些再分享。
|
请发表评论