Lua数据结构 — 闭包(四)

作者:罗日健

前面几篇文章已经说明了Lua里面很常用的几个数据结构,这次要分享的也是常用的数据结构之一 – 函数的结构。函数在Lua里也是一种变量,但是它却很特殊,能存储执行语句和被执行,本章主要描述Lua是怎么实现这种函数的。

在脚本世界里,相信闭包这个词大家也不陌生,闭包是由函数与其相关引用环境组成的实体。可能有点抽象,下面详细说明:

一、 闭包的组成

闭包主要由以下2个元素组成:

  1. 函数原型:上图意在表明是一段可执行代码。在Lua中可以是lua_CFunction,也可以是lua自身的虚拟机指令。
  2. 上下文环境:在Lua里主要是Upvalues和env,下面会有说明Upvalues和env。 在Lua里,我们也从闭包开始,逐步看出整个结构模型,下面是Closure的数据结构:(lobject.h 291-312)

不难发现,Lua的闭包分成2类,一类是CClosure,即luaC函数的闭包。另一类是LClosure,是Lua里面原生的函数的闭包。下面先讨论2者都有相同部分ClosureHeader:

  1. CommonHeader:和与TValue中的GCHeader能对应起来的部分
  2. isC:是否CClosure
  3. nupvalues:外部对象个数
  4. gclist:用于GC销毁,超出本章话题,在GC章节将详细说明
  5. env:函数的运行环境,下面会有补充说明

对于CClosure数据结构:

  1. lua_CFunction f:函数指针,指向自定义的C函数
  2. TValue upvalue[1]:C的闭包中,用户绑定的任意数量个upvalue

对于LClosure数据结构:

  1. Proto *p:Lua的函数原型,在下面会有详细说明
  2. UpVal *upvals:Lua的函数upvalue,这里的类型是UpVal,这个数据结构下面会详细说明,这里之所以不直接用TValue是因为具体实现需要一些额外数据。

 

二、 闭包的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)执行流程

  • 把3这个常量放到栈顶,执行FuncA

虚拟机操作:(帮助理解,与真实值有差别)

LOADK top 3                //把3这个常量放到栈顶
CALL  top FuncA nresults   //调用对应的FuncA函数
  • 虚拟机的pc已经在FuncA里面了,FuncA中的局部变量都是放到栈中的,所以第一句loacl c = 10是把10放到栈顶(这里假设先放到栈顶简化一些复杂细节问题,下同)

虚拟机操作:

LOADK top 10                //local c = 10
  • 遇到Function FuncB这个语句,会生成FuncB的闭包,这个过程同时会绑定upval到这个闭包上,但这是值还在栈上,upval只是个指针

上面生成一个闭包之后,因为在Lua里,函数也是一个变量,上面的语句等价于local FuncB = function() … end,所以也会生成一个临时的FuncB到栈顶。

虚拟机操作:



  • 最后return FuncB,就会把这个闭包关闭并返回出去,同时会把所有的upval进行unlink操作,让upval本身保存值

虚拟机操作:



2) FuncB的执行过程

到了FuncB执行的时候,参数b=5已经放到栈顶,然后执行FuncB。语句比较简单和容易理解,return a+b+c 虚拟机操作如下:


到这里UpVal的创建和使用也在上面给出事例说明,总结一下UpVal的实现:

  • UpVal是在函数闭包生成的时候(运行到function时)绑定的。
  • UpVal在闭包还没关闭前(即函数返回前),是对栈的引用,这样做的目的是可以在函数里修改对应的值从而修改UpVal的值,比如:

lua code:



  • 闭包关闭后(即函数退出后),UpVal不再是指针,而是。 知道UpVal的原理后,就只需要简要叙述一下UpVal的数据结构:(lobject.h 274 – 284)

  1. CommHeader: UpVal也是可回收的类型,一般有的CommHeader也会有
  2. TValue* v:当函数打开时是指向对应stack位置值,当关闭后则指向自己
  3. TValue value:函数关闭后保存的值
  4. UpVal* prev、UpVal* next:用于GC,全局绑定的一条UpVal回收链表

 

三、 函数原型

之前说的,函数原型是表明一段可执行的代码或者操作指令。在绑定到Lua空间的C函数,函数原型就是lua_CFunction的一个函数指针,指向用户绑定的C函数。下面描述一下Lua中的原生函数的函数原型,即Proto数据结构(lobject.h 231-253):

引用内容:

  1. CommonHeader:Proto也是需要回收的对象,也会有与GCHeader对应的CommonHeader
  2. TValue* k:函数使用的常量数组,比如local d = 10,则会有一个10的数值常量
  3. Instruction *code:虚拟机指令码数组
  4. Proto **p:函数里定义的函数的函数原型,比如funcA里定义了funcB,在funcA的5. Proto中,这个指针的[0]会指向funcB的Proto
  5. int *lineinfo:主要用于调试,每个操作码所对应的行号
  6. LocVar *locvars:主要用于调试,记录每个本地变量的名称和作用范围
  7. TString **upvalues:一来用于调试,二来用于给API使用,记录所有upvalues的名称
  8. TString *source:用于调试,函数来源,如c:\t1.lua@ main
  9. sizeupvalues: upvalues名称的数组长度
  10. sizek:常量数组长度
  11. sizecode:code数组长度
  12. sizelineinfo:lineinfo数组长度
  13. sizep:p数组长度
  14. sizelocvars:locvars数组长度
  15. linedefined:函数定义起始行号,即function语句行号
  16. lastlinedefined:函数结束行号,即end语句行号
  17. gclist:用于回收
  18. nups:upvalue的个数,其实在Closure里也有nupvalues,这里我也不太清楚为什么要弄两个,nups是语法分析时会生成的,而nupvalues是动态计算的。
  19. numparams:参数个数
  20. is_vararg:是否参数是”…”(可变参数传递)
  21. maxstacksize:函数所使用的stacksize

Proto的所有参数都是在语法分析和中间代码生成时获取的,相当于编译出来的汇编码一样是不会变的,动态性是在Closure中体现的。

 

四、 闭包运行环境

在前面说到的闭包数据结构中,有一个成员env,是一个Table*指针,用于指向当前闭包运行环境的Table。

什么是闭包运行环境呢?以下面代码举例:


上面代码中的d = 20,其实就是在环境变量中取env[“d”],所以env一定是个table,而当定义了本地变量之后,之后的所有变量都对从本地变量中操作。

 

五、 函数调用信息

函数调用相当于一个状态信息,每次函数调用都会生成一个状态,比如递归调用,则会有一个栈去记录每个函数调用状态信息,比如说下面这段没有意义的代码:


那么每次调用将会生成一个调用状态信息,上面代码会无限生成下去:

究竟一个CallInfo要记录哪些状态信息呢?下面来看看CallInfo的数据结构:

  1. Instruction *savedpc:如果这个调用被中断,则用于记录当前闭包执行到的pc位置
  2. nresults:返回值个数,-1为任意返回个数
  3. tailcalls:用于调试,记录尾调用次数信息,关于尾调用下面会有详细解释
  4. base、func、top:如下:

 

六、 函数调用的栈操作

上面描述的CallInfo信息,具体整个流程是怎么走的,结合下面代码详细地叙述整个调用过程,栈是怎么变化的:


假设现在走到了funcA(30, 40)这个语句,在执行前已经存在了global这个闭包和funcA这个闭包,在调用global这个闭包时,已经生成了一个global的CallInfo。

1) 函数调用的栈操作:(OP_CALL lvm.c 582-601)

  • global的CallInfo信息记录,并把funcA放到栈顶

当前虚拟机的pc指针,指向global函数原型中的CALL指令,这时global的CallInfo的savedpc就会保存当前pc。然后会把要执行的funcA的闭包放到栈顶。 – 参数分别放到栈顶(从左到右分别进栈),生成funcA的CallInfo,并把完成对应CallInfo栈操作

  • 设置虚拟机pc到funcA闭包第一条虚拟机Instruction,并继续执行虚拟机

2) 函数返回的栈操作:(OP_RETURN lvm.c 635-648)

  • 记录第一个返回值的位置到firstResult,把栈中的funcA位置设置为base和top

  • 把返回值根据nresult参数重新push到栈

  • 从全局CallInfo栈弹出funcA,并还原虚拟机pc到global的savedpc和栈信息

  • 继续执行虚拟机

 

七、 尾调用(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可能迟些再分享。

 

时间: 2024-10-26 01:43:16

Lua数据结构 — 闭包(四)的相关文章

Lua数据结构 — lua_State(六)

作者:罗日健 前面各种Lua的数据类型基本都说得差不多了,剩下最后一个数据类型:lua_State,我们可以认为是"脚本上下文",主要是包括当前脚本环境的运行状态信息,还会有gc相关的信息. Lua这门语言考虑了多线程的情况,在脚本空间中能够开多个线程相关脚本上下文,而大家会共用一个全局脚本状态数据,如下: 全局数据global_state的数据结构如下: global_state主要是用于GC的数据链表,下面简要说明几个: stringtable strt:这个是在TString那章

Lua数据结构 — Udata(五)

作者:罗日健 Udata负责存储userdata的数据,这部分其实很简单,但是为了保证系列文章的完整性,还是写一篇出来补全. 下面是Udata的数据结构: 意义: CommonHeader:和与TValue中的GCHeader能对应起来的部分 metatable:userdata的元表,和table的元表一样的 env:创建userdata时,会把当前执行语句的curenv赋给userdata的env,可修改 len:使用userdata的时候绑定对象申请的空间大小 和TString类似,用户绑

Lua数据结构 — TValue(一)

作者:罗日健 数据结构的设计,在一定程度上奠定了整个系统的设计,所以决定写一个对Lua主要数据结构的分析文章,本来打算写一篇就好了,但是每个数据类型其实都有点复杂,一篇的话篇幅太长,所以就拆开几篇来写了. 为什么是从TValue说起,TValue是实现Lua弱数据类型的主要数据结构,不但在脚本中的值使用了TValue,连Lua的实现中,很多数据结构也依赖于TValue,TValue一定程度上贯穿了整个Lua.先说一下Lua里面的数据类型:(lua.h :69) 从上面的定义中可以看到,Lua的值

Lua数据结构 — TString(二)

作者:罗日健 存储lua里面的字符串的TString数据结构:(lobject.h 196-207) 其它结构中也会有L_Umaxalign dummy这个东西,来看看L_Umaxaliagn: 从字面意思上就是保证内存能与最大长度的类型进行对齐,事实上也是做这件事,这里感觉lua想给各种不同设备做一种嵌入式脚本,这里要保证与最大的长度对齐能保证CPU运行高效不会罢工. tsv才是TString的主要数据结构: CommonHeader:这个是和GCObject能对应起来的GCheader re

Lua数据结构 — Table(三)

作者: 罗日健 前面(一).(二)里面其实已经把一些常用的数据类型(数值.布尔.字符串)说明了,这次要描述的是Table,Table在Lua里是一种常用的数据类型,是Lua里的精髓之一,其效率必须得到保证,而实现这种支持任意类型key和value的Table也是较为复杂的. 一, Table的设计思想: 1, 首先,讲一下Lua要设计的Table是怎么样子的: Lua就是想做这种支持任意类型的key和任意类型val的table,并且要高效和节约内存. 2, 基本的实现(基于链表的实现): 基于链

JavaScript中数据结构与算法(四):串(BF)

  这篇文章主要介绍了JavaScript中数据结构与算法(四):串(BF),串是由零个或多个字符组成的有限序列,又叫做字符串,本文着重讲解了BF(Brute Force)算法,需要的朋友可以参考下 串是由零个或多个字符组成的有限序列,又叫做字符串 串的逻辑结构和线性表很相似的,不同的是串针对是是字符集,所以在操作上与线性表还是有很大区别的.线性表更关注的是单个元素的操作CURD,串则是关注查找子串的位置,替换等操作. 当然不同的高级语言对串的基本操作都有不同的定义方法,但是总的来说操作的本质都

Lua中的闭包学习笔记_Lua

之前介绍 Lua 的数据类型时,也提到过,Lua 的函数是一种"第一类值(First-Class Value)".它可以: 存储在变量或 table (例如模块和面向对象的实现)里 复制代码 代码如下: t = { p = print } t.p("just a test!") 作为实参(也称其为"高阶函数(higher-order function)")传递给其他函数调用 复制代码 代码如下: t = {2, 3, 1, 5, 4} table

Lua中的闭包小结_Lua

前言 在很多语言中都有闭包的概念,而在这里,我将主要对Lua语言的闭包概念进行分析与总结.希望对大家学习Lua有帮助. 什么是闭包? 闭包在Lua中是一个非常重要的概念,闭包是由函数和与其相关的引用环境组合而成的实体.我们再来看一段代码: 复制代码 代码如下: function newCounter()      local i = 0      return function () -- 匿名函数           i = i + 1           return i      end

Lua之Lua数据结构-TTLSA(6)(转) good

一. tabletable是lua唯一的数据结构.table 是 lua 中最重要的数据类型. table 类似于 python 中的字典.table 只能通过构造式来创建.其他语言提供的其他数据结构如array.list等等,lua都是通过table来实现的.table非常实用,可以用在不同的情景下.最常用的方式就是把table当成其他语言的数组.实例1:   1 2 3 4 mytable = {} for index = 1, 100 do     mytable[index] = mat