lua的函数、闭包、函数调用

  • lua存在三类函数,大类都是LUA_TFUNCTION,变体分别是LUA_VLCL(Lua closure)、LUA_VLCF(light C function)、LUA_VCCL(C closure)。
  • Lua脚本函数(LUA_VLCL)。用lua脚本写的函数。运行时需要闭包LClosure,函数原型Proto。
  • 轻量C函数(LUA_VCCL)。用C写,没有上值。运行时不须要闭包,函数原型lua_CFunction。
  • C闭包函数(LUA_VLCF)。用C写,有上值。运行时需要闭包CClosure,函数原型lua_CFunction。

函数在Lua里也是一种变量,但是它却很特殊,能存储执行语句和被执行,本章主要描述Lua是怎么实现这种函数的。

闭包是由函数和其相关引用环境(上值)组成的实体。可能有点抽象,下面详细说明:

一、闭包的组成

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

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

typedef struct CClosure {
  ClosureHeader;
  lua_CFunction f;
  TValue upvalue[1];  /* list of upvalues */
} CClosure;

typedef struct LClosure {
  ClosureHeader;
  struct Proto *p;
  UpVal *upvals[1];  /* list of upvalues */
} LClosure;

typedef union Closure {
  CClosure c;
  LClosure l;
} Closure; 

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

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

对于CClosure数据结构(C闭包函数):

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

对于LClosure数据结构(Lua脚本函数):

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

 

二、闭包的UpVal实现

转自lua数据结构--闭包。未核对,当中有错误。留着只是为将来做参数。

究竟什么是UpVal呢?先来看看代码:

function FunA(a)
  local c = 10
  function FuncB(b)
    return a + b + c
  end
  return FuncB
end

local testA = FuncA(3)
local testB = testA(5)

分析一下上面这段代码,最终testB的值显然是3+5+10=18。当调用testA(5)的时候,其实是在调用FuncB(5),但是这个FuncB知道a = 3,这个是由FuncA调用时,记录到FuncB的外部变量,我们把a和c称为FuncB的upvalue。那么Lua是如何实现upvalue的呢? 以上面这段代码为例,从虚拟机的角度去分析实现流程:

2.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到栈顶。

虚拟机操作:

NEWCLOSURE // 寄存器指定的位置就是栈顶,用来存储生成的FuncB Closure,是一上个闭包,就是FuncB的闭包
GETUPVAL  // FuncB闭包的upvals添加一个指针指向d
GETUPVAL  // FuncB闭包的upvals添加一个指针指向c
  • 最后return FuncB,就会把这个闭包关闭并返回出去,同时会把所有的upval进行unlink操作,让upval本身保存值

虚拟机操作:

RETURN r // 不嫌脏闭包关闭并退出,清栈并把返回值r放到栈顶

2.2 FuncB的执行过程

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

GETUPVAL   // 获了当前闭包(FuncB)的
LOADK        // 把栈顶的值
ADD
GETUPVAL
ADD
RETURN     // 闭包关闭,把ra的值放到栈顶后返回

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

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

    在上面的例子中,testB = 100。因为函数还没有关闭,所以在funcB查找UpVal会找到对应修改后引用。

  • 闭包关闭后(即函数退出后),UpVal不再是指针,而是。 知道UpVal的原理后,就只需要简要叙述一下UpVal的数据结构。
<lua>/lobject.h
------
typedef struct UpVal {
  CommonHeader;
  lu_byte tbc;  /* true if it represents a to-be-closed variable */
  TValue *v;  /* points to stack or to its own value */
  union {
    struct {  /* (when open) */
      struct UpVal *next;  /* linked list */
      struct UpVal **previous;
    } open;
    TValue value;  /* the value (when closed) */
  } u;
} UpVal;
  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数据结构

引用内容:

<lua>/lobject.h
------
typedef struct Proto {
  CommonHeader;
  lu_byte numparams;  /* number of fixed (named) parameters */
  lu_byte is_vararg;
  lu_byte maxstacksize;  /* number of registers needed by this function */
  int sizeupvalues;  /* size of 'upvalues' */
  int sizek;  /* size of 'k' */
  int sizecode;
  int sizelineinfo;
  int sizep;  /* size of 'p' */
  int sizelocvars;
  int sizeabslineinfo;  /* size of 'abslineinfo' */
  int linedefined;  /* debug information  */
  int lastlinedefined;  /* debug information  */
  TValue *k;  /* constants used by the function */
  Instruction *code;  /* opcodes */
  struct Proto **p;  /* functions defined inside the function */
  Upvaldesc *upvalues;  /* upvalue information */
  ls_byte *lineinfo;  /* information about source lines (debug information) */
  AbsLineInfo *abslineinfo;  /* idem */
  LocVar *locvars;  /* information about local variables (debug information) */
  TString  *source;  /* used for debug information */
  GCObject *gclist;
} Proto;
  1. CommonHeader:Proto也是需要回收的对象,也会有与GCHeader对应的CommonHeader
  2. TValue* k:函数使用的常量数组,比如local d = 10,则会有一个10的数值常量。sizek指示该数组长度。
  3. Instruction *code:虚拟机指令码数组
  4. Proto **p:函数里定义的函数的函数原型,比如funcA里定义了funcB,在funcA的5. Proto中,这个指针的[0]会指向funcB的Proto
  5. int *lineinfo:主要用于调试,但不是此个操作码所在行号,而是此个操作码所在行号减去上一个操作码所在行号
  6. LocVar *locvars:主要用于调试,记录每个本地变量的名称和作用范围
  7. Upvaldescg **upvalues:记录运行该函数时要用到的upvalues
  8. TString *source:用于调试,函数来自哪个脚本文件,如“@t1.lua”
  9. sizeupvalues: upvalues数组长度
  10. sizek:常量数组(k)长度
  11. sizecode:code数组长度
  12. sizelineinfo:lineinfo数组长度
  13. sizep:p数组长度
  14. sizelocvars:locvars数组长度
  15. linedefined:函数定义起始行号,即“function”这个关键字所在行号
  16. lastlinedefined:函数结束行号,即“end”这个关键字所在行号
  17. gclist:用于回收
  18. numparams:参数个数
  19. is_vararg:是否参数是”…”(可变参数传递)
  20. maxstacksize:执行此个函数须要的栈单元数。前面的b个栈单元用于存储函数的固定参数。参数是可变时,不包括可变部分。

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

 

四、闭包运行环境

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

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

function funcA()
  d = 20         // 在运行环境的table中设置d变量,即env["d"] = 20
  local d = 50   // 在本地创建一个变量叫d 
  local c = d    // 由于创建了本地主量d,所以会在本地读取d变量
end

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

 

五、 函数调用信息

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

function funcA()
  funcA()
end

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

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

<lua>/lstate.h
------
typedef struct CallInfo {
  StkId func;  /* function index in the stack */
  StkId	top;  /* top for this function */
  struct CallInfo *previous, *next;  /* dynamic call link */
  union {
    struct {  /* only for Lua functions */
      const Instruction *savedpc;
      volatile l_signalT trap;
      int nextraargs;  /* # of extra arguments in vararg functions */
    } l;
    struct {  /* only for C functions */
      lua_KFunction k;  /* continuation in case of yields */
      ptrdiff_t old_errfunc;
      lua_KContext ctx;  /* context info. in case of yields */
    } c;
  } u;
  union {
    int funcidx;  /* called-function index */
    int nyield;  /* number of values yielded */
    struct {  /* info about transferred values (for call/return hooks) */
      unsigned short ftransfer;  /* offset of first value transferred */
      unsigned short ntransfer;  /* number of values transferred */
    } transferinfo;
  } u2;
  short nresults;  /* expected number of results from this function */
  unsigned short callstatus;
} CallInfo;
  1. Instruction *savedpc:如果这个调用被中断,则用于记录当前闭包执行到的pc位置
  2. nresults:返回值个数,-1为任意返回个数
  3. tailcalls:用于调试,记录尾调用次数信息,关于尾调用下面会有详细解释
  4. func、top:另有一个常用变量base,它总是func+1。如下:

 

六、函数调用的栈操作

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

function global()
  function funcA(a, b)
    return a + b + 10
  end
  functionA(30, 40)
end

global()、funcA()内中是什么虚拟机指令见“lua脚本函数原型Proto,示例”。假设现在走到了funcA(30, 40)这个语句,在执行前已经存在了global这个闭包和funcA这个闭包,在调用global这个闭包时,已经生成了一个global的CallInfo。

6.1 函数调用的栈操作:(OP_CALL)

6.3 luaV_execute、luaD_precall、luaD_poscall有对相关函数注释。

1、lua虚拟机(VM)依次把被调函数funcA的闭包压栈(GETTABUP),参数压栈(LOADI、LOADI),调用funcA(OP_CALL)。执行这些指令不会跳出luaV_execute内的for循环。

2:L->top = ra + b。ra = base+GETARG_A(i),指向此次指令受影响的栈单元。对OP_CALL,就就funcA所在的栈单元。b是参数个数。

3:ci->u.l.savedpc = pc。存储执行完被调函数funcA后,继续执行的是哪条pc。ci是调用函数的CallInfo,这个savedpc用于后面的执行OP_RETURN时。

4:ci = next_ci(L)。从内存分配出sizeof(CallInfo)字节的内存块,用于存储此次调用的CallInfo,并填写此次的调用信息。

  1. ci->nresults = nresults。
  2. ci->u.l.savedpc = p->code。被调函数funcA的第一条指令保存到savedpc,后绪startfunc将用此个savedpc更新VM pc。
  3. ci->top = func + 1 + fsize。fsize值是maxstacksize。maxstacksize是执行此个函数须要的栈单元数,其前面b个栈单元用于存储参数。OP_CALL后,ci->top = func + 1 + p->maxstacksize。但L->top指向的是L->top = ra + b。
  4. ci->func = func。

5:L->ci = ci。L->ci指向当前函数的CallInfo,接下要认为被调函数是当前函数。

6:goto startfunc。跳出for循环,从startfunc入口开始执行。

7:pc = ci->u.l.savedpc。ci指的是被调函数,pc指向被调函数Proto的第一条指令,即faucA的第一条指令:ADD。

8:base = ci->func + 1。base总是指向ci->func+1。

9:进入for循环,VM开始执行被调函数funcA。

6.2 函数返回的栈操作:(OP_RETURN)

6.3 luaV_execute、luaD_precall、luaD_poscall有对相关函数注释。

1:n = GETARG_B(i) - 1。n是被调函数的返回值个数,即后绪的nres。

2:L->ci = ci->previous。

3:moveresults(L, ci->func, nres, ci->nresults)。引入一个变量firstresult:firstresult = L->top - nres。把firstresult开始的nres个栈单元移动到ci->func开始的nres个单元。注意,此处的ci还是被调函数funcA的CallInfo,ci->func就是之前存放着funcA的栈单元。对要复制内容,示例就一个返回值:80。

4:ci = ci->previous。虽然funcA的CallInfo没被内存回收,但此次调用已结束,后面不会用到它了。

5:goto returning。跳出for循环,从returning入口开始执行。

6:pc = ci->u.l.savedpc。ci指的是调用函数,pc会调用函数OP_CALL的下一条指令。并还原虚拟机pc到global的savedpc和栈信息。

8:base = ci->func + 1。base恢复到调用函数的ci->func+1。

9:进入for循环,——至此VM执行调用函数OP_CALL的下一条指令。

6.3 luaV_execute、luaD_precall、luaD_poscall

void luaV_execute (lua_State *L, CallInfo *ci) {
  LClosure *cl;
  TValue *k;
  StkId base;
  const Instruction *pc;
  int trap;
#if LUA_USE_JUMPTABLE
#include "ljumptab.h"
#endif
 // 执行一些指令后,像OP_CALL、OP_TAILCALL,不是继续for内继续循环,而是跳到startfunc。
 // 跳到startfunc一个目的是要让更新pc不是简单的+1,而是获取自ci->u.l.savedpc。
 startfunc:
  trap = L->hookmask;
 returning:  /* trap already set */
  cl = clLvalue(s2v(ci->func));
  k = cl->p->k;
  // 此处是给VM-pc赋初值。这里分两种情况,1)startfunc时,ci指的是被调函数,pc会是被调函数Proto的第一条指令。   
  // 2)returning时,ci指的是调用函数,pc会是调用函数Proto中OP_CALL后的那条指令。
  pc = ci->u.l.savedpc;
  if (trap) {
    if (pc == cl->p->code) {  /* first instruction (not resuming)? */
      if (cl->p->is_vararg)
        trap = 0;  /* hooks will start after VARARGPREP instruction */
      else  /* check 'call' hook */
        luaD_hookcall(L, ci);
    }
    ci->u.l.trap = 1;  /* assume trap is on, for now */
  }
  // base总是指向ci->func+1,因为它对在栈中定位非常重要,低版本lua还把它定义为CallInfo的一个字段。
  base = ci->func + 1;
  /* main loop of interpreter */
  for (;;) {
    Instruction i;  /* instruction being executed */
    StkId ra;  /* instruction's A register */
    // vmfetch执行两个操作,1)让i指示当前要执行的指令,2)让ra指向此次指令受影响的栈单元
    // i = *(pc++); 
    // ra = base+GETARG_A(i)
    vmfetch();
    lua_assert(base == ci->func + 1);
    lua_assert(base <= L->top && L->top < L->stack_last);
    /* invalidate top for instructions not expecting it */
    lua_assert(isIT(i) || (cast_void(L->top = base), 1));
    vmdispatch (GET_OPCODE(i)) {
      ..
      vmcase(OP_CALL) {
        CallInfo *newci;
        int b = GETARG_B(i);
        int nresults = GETARG_C(i) - 1;
        if (b != 0)  /* fixed number of arguments? */
          L->top = ra + b;  /* top signals number of arguments */
        /* else previous instruction set top */
        // savepc的目的是把pc存储在ci->u.l.savedpc。通过vmfetch,此时pc指向OP_CALL后的那条指令。
        // ci->u.l.savedpc = pc
        savepc(L);  /* in case of errors */
        if ((newci = luaD_precall(L, ra, nresults)) == NULL) {
          // 返回值NULL,意味被调函数是轻量C函数、或C闭包函数。
          // 对这两类函数,能执行到这里意味着已执行完被调函数,
          // VM应该执行的下一条指令是OP_CALL后的下一条指令,因而不必改pc
          updatetrap(ci);  /* C call; nothing else to be done */
        } else {  /* Lua call: run function in this same C frame */
          ci = newci;
          ci->callstatus = 0;  /* call re-uses 'luaV_execute' */
          // 被调函数是lua脚本函数,跳到startfunc,让VM pc会是被调函数Proto的第一条指令。
          goto startfunc;
        }
        vmbreak;
      }
      ...
      vmcase(OP_RETURN) {
        int n = GETARG_B(i) - 1;  /* number of results */
        int nparams1 = GETARG_C(i);
        if (n < 0)  /* not fixed? */
          n = cast_int(L->top - ra);  /* get what is available */
        savepc(ci);
        if (TESTARG_k(i)) {  /* may there be open upvalues? */
          if (L->top < ci->top)
            L->top = ci->top;
          luaF_close(L, base, CLOSEKTOP, 1);
          updatetrap(ci);
          updatestack(ci);
        }
        if (nparams1)  /* vararg function? */
          ci->func -= ci->u.l.nextraargs + nparams1;
        L->top = ra + n;  /* set call for 'luaD_poscall' */
        // luaD_poscall有两个任务。
        // 1)L->ci = ci->previous。
        // 2)moveresults(L, ci->func, nres, ci->nresults)。在栈中移动被调函数的n个返回值。
        luaD_poscall(L, ci, n);
        updatetrap(ci);  /* 'luaD_poscall' can change hooks */
        goto ret;
      }
      vmcase(OP_RETURN1) {
        ...
       ret:  /* return from a Lua function */
        if (ci->callstatus & CIST_FRESH) {
          // 进入此处,意味着是C调用lua脚本函数。参考ldo.c中的ccall(...)。
          return;  /* end this frame */
        } else {
          // luaD_poscall改的是L->ci,此处改ci。
          ci = ci->previous;
          // OP_CALL已经把调用函数ci的savedpc指向OP_CALL的下一条指令。
          // returning时,ci指的是调用函数,pc会是调用函数Proto中OP_CALL后的那条指令
          goto returning;  /* continue running caller in this frame */
        }
      }
      ...
    }
  }
}

CallInfo *luaD_precall (lua_State *L, StkId func, int nresults) {
  lua_CFunction f;
 retry:
  switch (ttypetag(s2v(func))) {
    case LUA_VCCL:  /* C closure */
      f = clCvalue(s2v(func))->f;
      goto Cfunc;
    case LUA_VLCF:  /* light C function */
      f = fvalue(s2v(func));
     Cfunc: {
      int n;  /* number of returns */
      CallInfo *ci;
      checkstackGCp(L, LUA_MINSTACK, func);  /* ensure minimum stack size */
      // next_ci注释见“case LUA_VLCL”部分。
      L->ci = ci = next_ci(L);
      ci->nresults = nresults;
      ci->callstatus = CIST_C;
      ci->top = L->top + LUA_MINSTACK;
      ci->func = func;
      lua_assert(ci->top <= L->stack_last);
      if (L->hookmask & LUA_MASKCALL) {
        int narg = cast_int(L->top - func) - 1;
        luaD_hook(L, LUA_HOOKCALL, -1, 1, narg);
      }
      lua_unlock(L);
      // 不论C闭包函数,还是轻量C函数,函数原型都是lua_CFunction,执行这个f。
      n = (*f)(L);  /* do the actual call */
      // 到此已执行完被调函数。
      lua_lock(L);
      api_checknelems(L, n);
      luaD_poscall(L, ci, n);
      // C闭包函数、轻量C函数,luaD_precall返回NULL。
      return NULL;
    }
    case LUA_VLCL: {  /* Lua function */
      CallInfo *ci;
      Proto *p = clLvalue(s2v(func))->p;
      int narg = cast_int(L->top - func) - 1;  /* number of real arguments */
      int nfixparams = p->numparams;
      int fsize = p->maxstacksize;  /* frame size */
      checkstackGCp(L, fsize, func);
      // #define next_ci(L)  (L->ci->next ? L->ci->next : luaE_extendCI(L))
      // L->ci是调用的函数的CallInfo,当它存在next时,next_ci并不是“完全”使用该ci,
      // 只是用它已分配的sizeof(CallInfo)字节内存块,免得再次分配而已。
      L->ci = ci = next_ci(L);
      // next_ci已从内存分配出sizeof(CallInfo)字节的内存块,接下填写此次的调用信息。
      ci->nresults = nresults;
      // 调用函数的第一条指令保存到savedpc,后绪startfunc将用此个savedpc更新VM pc。
      ci->u.l.savedpc = p->code;  /* starting point */
      // maxstacksize是执行此个函数须要的栈单元数,其前面b个栈单元用于存储参数。
      // OP_CALL后,ci->top = func + 1 + p->maxstacksize。但L->top指向的是L->top = ra + b。
      ci->top = func + 1 + fsize;
      ci->func = func;
      L->ci = ci;
      for (; narg < nfixparams; narg++)
        setnilvalue(s2v(L->top++));  /* complete missing arguments */
      lua_assert(ci->top <= L->stack_last);
      // 对lua脚本函数,luaD_precall返回被调函数的CallInfo。
      return ci;
    }
    default: {  /* not a function */
      checkstackGCp(L, 1, func);  /* space for metamethod */
      luaD_tryfuncTM(L, func);  /* try to get '__call' metamethod */
      goto retry;  /* try again with metamethod */
    }
  }
}

void luaD_poscall (lua_State *L, CallInfo *ci, int nres) {
  if (L->hookmask)
    L->top = rethook(L, ci, L->top - nres, nres);
  // 恢复L->ci,让指回调用函数。
  L->ci = ci->previous;  /* back to caller */
  /* move results to proper place */
  // 把firstresult(L->top - nres)开始的nres个栈单元移动到ci->func开始的nres个单元。
  moveresults(L, ci->func, nres, ci->nresults);
}

 

七、尾调用(TAILCALL)

转自https://www.cnblogs.com/freebird92/p/6761970.html。未核对,当中有错误。留着只是为将来做参考。

function Recursion(a)
  if a == 1 then
    return 1
  end
  return Recursion(a - 1) + 1
end

如果是Recursion(10)的话,能正确计算出1+2 * 10的值,但是如果是Recursion(20000),这样毫无疑问会导致stack overflow。

就像上一节说到的,每个递归调用会生成一个CallInfo,全局CallInfo栈的大小是有限的,基于乘2增长可以知道lua栈最大深度是16834(2^14):

<lua>/luaconf.h
------ (Lua5.0.4已没有这个宏定义)
#define LUAI_MAXCALLS	20000

尾调用是一种对函数解释的优化方法,对于上面代码,改造成下面代码后,则不会出现stack overflow:

function Recursion(n, a)
  if n == 1 then
    return n + 1
  end
  if a == nil then
    n = a
  end
  return Recursion(n - 1, a + n)
end

上面的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可能迟些再分享。

全部评论: 0

    写评论: