lua5.3 for 循环实现分析

平时用 for 循环的地方挺多的,所以有空就想着看看它内部实现原理。

根据 lua 语法规定,for 循环分为两类:

  • 数值for循环
  • 泛型for循环

数值for循环

格式:

for var=exp1,exp2,exp3 do  
    <执行体>  
end

解析阶段分析:

static void forstat (LexState *ls, int line) {
  /* forstat -> FOR (fornum | forlist) END */
  ...
  luaX_next(ls);  /* skip 'for' */
  varname = str_checkname(ls);  /* first variable name ;获取 for 后面的第一个变量,并解析下一个单词,存放在 ls->t 中 */
  switch (ls->t.token) {
    // 如果当前解析到的单词是 '=',那么就走数值for函数
    case '=': fornum(ls, varname, line); break;
    // 如果当前解析到的单词是 ',',那么就走泛型for函数
    case ',': case TK_IN: forlist(ls, varname); break;
  ...
}

static void fornum (LexState *ls, TString *varname, int line) {
  /* fornum -> NAME = exp1,exp1[,exp1] forbody */
  FuncState *fs = ls->fs;
  int base = fs->freereg;
  // 默认生成三个变量名,用来控制for循环,以及一个用户自定义计数器变量 varname
  new_localvarliteral(ls, "(for index)");
  new_localvarliteral(ls, "(for limit)");
  new_localvarliteral(ls, "(for step)");
  new_localvar(ls, varname);
    
  checknext(ls, '=');
  exp1(ls);  /* initial value */
  checknext(ls, ',');
  exp1(ls);  /* limit */
  if (testnext(ls, ','))
    exp1(ls);  /* optional step */
  else {  /* default step = 1 */
    luaK_codek(fs, fs->freereg, luaK_intK(fs, 1));
    luaK_reserveregs(fs, 1);
  }
  forbody(ls, base, line, 1, 1);
}

可以看到,判断是走数值for循环解析,还是走泛型for循环解析,是根据 for 后面第一次出现 ',' 还是 '=' 决定的。

fornum 函数中看到,for var=exp1,exp2,exp3 do 默认会生成三个变量,由它们来控制循环,它们对使用者来说,是不可见的。这三个变量分别是:

  • "(for index)" 记录当前循环次数。
  • "(for limit)" 控制循环总次数。
  • "(for step)" 控制循环步长,每进入循环体一次后,index 要增加或减少多少。如果用户不指定,就默认生成为1。

同时还会生成一个用户指定的计数器 var,如下图所示:

接下来看看 forbody 函数中,会生成哪些指令。

static void forbody (LexState *ls, int base, int line, int nvars, int isnum) {
  /* forbody -> DO block */
  ...
  // 解析循环体 body 前,先生成一条 OP_FORPREP 指令
  prep = isnum ? luaK_codeAsBx(fs, OP_FORPREP, base, NO_JUMP) : luaK_jump(fs);
  ...
  block(ls); // 解析for循环体 body 里的代码
  ...
  // 解析 body 后,再生成一条 OP_FORLOOP 指令
  if (isnum)  /* numeric for? */
    endfor = luaK_codeAsBx(fs, OP_FORLOOP, base, NO_JUMP);
  ...
}

在进入循环体前,会先生成一条 OP_FORPREP 指令, 这个指令主要是初始化 forindex,该值为 exp1 - exp3,然后跳转到 OP_FORLOOP 指令。OP_FORLOOP 指令作用是 forindex  += forstep, 然后判断 forindex 是否超过了 forlimit 值,如果没有,跳到 forbody 里面,执行代码指令,如果超过了,就跳出循环。

 

 运行时,执行指令逻辑是在 lvm.c 的 lua_execute 函数中,我们可以用伪代码简单分析下 OP_FORPREP ,OP_FORLOOP  指令实现:

case OP_FORPREP: {
    TValue *init = ra; // 获取 exp1 初始值
    TValue *step = ra + 2; // 获取 exp3 步长值
    init->value_.i -= step->value_.i; // 初始化 forindex = exp1 - step
    ci->u.l.savedpc += GETARG_sBx(i); // 跳到 OP_FORLOOP 指令处
    break;
}
case OP_FORLOOP: {
    lua_Integer step = ivalue(ra + 2);  // 获取步长
    lua_Integer idx = ra->value_.i + step; // forindex 计数器 + 步长step
    lua_Integer limit = ivalue(ra + 1); // 获取 forlimit 循环边界
    if ((0 < step) ? (idx <= limit) : (limit <= idx)) {  // 如果 forindex 没有超过 forlimit
        ci->u.l.savedpc += GETARG_sBx(i); // 跳转到 forbody 执行循环体里的指令
        ra->value_.i = idx; // 更新 forindex 计数器值
        setivalue(ra + 3, idx);  // 同时更新用户自定义计数器值
    }
    break;
}

可以看到,在处理 OP_FORPREP  中,forindex 初始化时,先减去一次步长,然后立马跳到 OP_FORLOOP 指令处理时,又加了一次步长,相当于 forindex 没有改变其值。这么做,我觉得是为了在 OP_FORLOOP 指令中,forindex 能正常累计循环次数。

每执行一次 forbody 里的代码后,都会按顺序执行到 OP_FORLOOP 指令,forindex 以及 var 计数器都会累加步长,然后判断 forindex 是否超出 forlimit 范围,超出就会继续按顺序往下走,否则就跳回到 forbody 里继续执行循环体里的代码。

下面简单看下 数值for 例子:

for i=1,3 do
    print(i)
end

luac -l -l aaa.lua 查看其对应的指令:

main <aaa.lua:0,0> (9 instructions at 00000000007584c0)
0+ params, 6 slots, 1 upvalue, 4 locals, 3 constants, 0 functions
        1       [1]     LOADK           0 -1    ; 1
        2       [1]     LOADK           1 -2    ; 10
        3       [1]     LOADK           2 -1    ; 1
        4       [1]     FORPREP         0 3     ; to 8
        5       [2]     GETTABUP        4 0 -3  ; _ENV "print"
        6       [2]     MOVE            5 3
        7       [2]     CALL            4 2 1
        8       [1]     FORLOOP         0 -4    ; to 5
        9       [3]     RETURN          0 1
constants (3) for 00000000007584c0:
        1       1
        2       10
        3       "print"
locals (4) for 00000000007584c0:
        0       (for index)     4       9
        1       (for limit)     4       9
        2       (for step)      4       9
        3       i       5       8
upvalues (1) for 00000000007584c0:
        0       _ENV    1       0

可以看到 locals 那列,一共有4个变量。前面三个变量是 lua 默认生成的,对用户不可见。只有第4个变量 i 是可见的。在指令列表中,第 4 条 FORPREP 指令初始化 forindex  = 1 - 1 后,立马跳到第 8 条 FORLOOP 指令执行 forindex += 1 运算,判断 forindex 是否小于等于 forlimit = 10,如果满足条件,就反方向跳回到第 5 条指令,获取 print 函数,执行 print(i) 操作。print(i) 执行完后,按顺序又会执行到第 8 条指令 FORLOOP,forindex += 1,直到 forindex 大于 forlimit 时,才会跳出循环,执行第 9 条指令 return 返回。

我们再看一个例子,for 循环 5次,其中如果 i 为 3 时,将 i 重置为为 1,我们发现 for 不会无限循环,第 4 次循环时,i 又被覆盖回去了,变成了 4,而不是一直保持 1。这就是因为有 forindex 计数器的存在,forindex 计数器控制着循环的进度,不会依赖用户自定义的 i,i 只是 forindex 的一个副本,所以也就不会有死循环,i 每次进循环体前,都会被 forindex  覆盖。

for i=1,5 do
    if i == 3 then
        i = 1
    end
    print(i)
end

--[[
运行结果:
1
2
1
4
5
]]

泛型for循环

格式:

for <var-list> in <exp-list> do
    <body>
end

<var-list> 可以是一个或多个 var 组成,由逗号','分隔,<exp-list> 也可以由多个表达式 exp 组成,同样由逗号','分隔,但一般我们只用到一个 exp。

 接下来,同样对解析阶段进行分析。先看看源码是如何对 for <var-list> in <exp-list> do 进行解析的:

static void forstat (LexState *ls, int line) {
  /* forstat -> FOR (fornum | forlist) END */
  ...
  varname = str_checkname(ls);  /* first variable name */
  switch (ls->t.token) {
    case '=': fornum(ls, varname, line); break;
    case ',': case TK_IN: forlist(ls, varname); break;
    ...
}

static void forlist (LexState *ls, TString *indexname) {
  /* forlist -> NAME {,NAME} IN explist forbody */
  FuncState *fs = ls->fs;
  expdesc e;
  int nvars = 4;  /* gen, state, control, plus at least one declared var */
  int line;
  int base = fs->freereg;
  /* create control variables */
  new_localvarliteral(ls, "(for generator)");
  new_localvarliteral(ls, "(for state)");
  new_localvarliteral(ls, "(for control)");
  /* create declared variables */
  new_localvar(ls, indexname);
  while (testnext(ls, ',')) {
    new_localvar(ls, str_checkname(ls));
    nvars++;
  }
  checknext(ls, TK_IN);
  line = ls->linenumber;
  adjust_assign(ls, 3, explist(ls, &e), &e);
  luaK_checkstack(fs, 3);  /* extra space to call generator */
  forbody(ls, base, line, nvars - 3, 0);
}

在进入 forlist 函数时,同样会生成 3 个默认变量,对用户不可见。

  • "(for generator)":指向迭代器函数
  • "(for state)" :指向一个不可变状态
  • "(for control)":控制变量

迭代器:是一种能让我们遍历集合所有元素的代码结构。在 lua 中,通常使用函数来表示迭代器:每次调用函数时,函数会返回集合中的下一个元素。所有的迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置和下一次遍历时的位置。

接着回到 for <var-list> in <exp-list> do,从源码中可以看到,泛型 for 是按顺序解析,先生成3个变量,来控制着循环的进行,对用户不可见,然后再接着解析用户自定义变量列表,以及 in 后面的表达式,in 后的表达式,返回的结果,就是保存在 (for generator),(for state),(for control) 这3个不可见的变量中。如果表达式 exp-list 返回超过3个以上的值,多出来的会被抛弃掉,不足3个会被补 nil。例如,表达式可以只返回一个闭包函数做为迭代器, 用 闭包的上值 来保存不可变状态,以及控制变量,这样就只需要返回一个迭代函数(闭包函数)就可以了,因此,不可变状态和控制变量都为 nil。

那我们自定义的变量列表 var-list 是什么时候被赋值的呢,根据 lua 规定,在初始化上面步骤,获取到迭代器,不可变状态,控制变量后,for 循环会将 不可变状态,控制变量 做为参数传递给迭代器调用。迭代器返回的值,就会赋值给我们自定义的变量列表 var-list 了。直到迭代器返回的第一个值为 nil,才退出循环。我们可以通过下面等价的 lua 伪代码来解释泛型 for 实现机制,可能会更清晰些。

do
    local f, s, step = explist -- 通过表达式列表返回迭代器,不可变状态,控制变量
    while true do
        local var_1, var_2, ..., var_n = f(s, step) -- 接着调用迭代器,参数:不可变状态,控制变量
        step = var_1
        if step == nil then -- 迭代器返回的第一个值,如果为 nil,退出循环
            break
        end

        block --循环体 body 部分逻辑代码
    end
end

在解析完 for <var-list> in <exp-list> do之后,就进入到 forbody 部分了,这部分和数值 for 循环类似,都会在解析 body 前后,生成一些指令,配合 迭代器 来控制循环是否要结束。

static void forbody (LexState *ls, int base, int line, int nvars, int isnum) {
  /* forbody -> DO block */
  ...
  prep = isnum ? luaK_codeAsBx(fs, OP_FORPREP, base, NO_JUMP) : luaK_jump(fs);
  ...
  block(ls);
  ...
    luaK_codeABC(fs, OP_TFORCALL, base, 0, nvars);
    luaK_fixline(fs, line);
    endfor = luaK_codeAsBx(fs, OP_TFORLOOP, base + 2, NO_JUMP);
  ...
}

int luaK_jump (FuncState *fs) {
  ...
  j = luaK_codeAsBx(fs, OP_JMP, 0, NO_JUMP);
  ...
}

在进入 body 前会调用 luaK_jump 函数,先生成一条直接跳转指令 OP_JMP。它的作用是直接跳到 OP_TFORCALL 指令处,初始化表达式,获取迭代器、不可变状态、控制变量 这三个值。OP_TFORLOOP 的作用,就像上面的 lua 伪代码一样,在获取到迭代器等这三个变量后,调用迭代器,变量列表 var-list 接收其返回值,接着判断第一个变量值是否为 nil,如果是,跳出循环,如果不是,则跳到循环体内执行相应逻辑。

运行阶段分析:

我们通过一个简单的例子,来分析泛型 for 循环在运行阶段是如何实现的

local t = {1, "nice", false}

for i, v in pairs(t) do
    print(i,v)
end

接着查看下其对应的指令生成,luac -l -l aaa.lua

main <..\aaa.lua:0,0> (16 instructions at 00b672f8)
0+ params, 9 slots, 1 upvalue, 6 locals, 4 constants, 0 functions
        -- 1-5条指令,对应 local t = {1, "nice", false}
        1       [1]     NEWTABLE        0 3 0
        2       [1]     LOADK           1 -1    ; 1
        3       [1]     LOADK           2 -2    ; "nice"
        4       [1]     LOADBOOL        3 0 0
        5       [1]     SETLIST         0 3 1   ; 1
        6       [3]     GETTABUP        1 0 -3  ; _ENV "ipairs"
        7       [3]     MOVE            2 0
        -- 调用执行 ipair(t)
        8       [3]     CALL            1 2 4
        -- 在调用完 ipair(t) 后,返回迭代器 ipairsaux,不变状态 t,以及一个控制变量 0,接着立马跳转到第14条指令 TFORCALL
        9       [3]     JMP             0 4     ; to 14
        10      [4]     GETTABUP        6 0 -4  ; _ENV "print"
        11      [4]     MOVE            7 4
        12      [4]     MOVE            8 5
        13      [4]     CALL            6 3 1
        -- 判断控制(for control) 是否为 nil,不为 nil 则跳到第 11 条指令执行 print(i,v),如果为nil,则接着往下执行,算是跳出循环了
        14      [3]     TFORCALL        1 2
        15      [3]     TFORLOOP        3 -6    ; to 10
        16      [5]     RETURN          0 1
constants (4) for 00b672f8:
        1       1
        2       "nice"
        3       "ipairs"
        4       "print"
locals (6) for 00b672f8:
        0       t       6       17
        1       (for generator) 9       16
        2       (for state)     9       16
        3       (for control)   9       16
        4       i       10      14
        5       v       10      14
upvalues (1) for 00b672f8:
        0       _ENV    1       0

接着对比源码,我简化其源码实现,目的是为了方便理解,具体可以查看 lvm.c 中的 luaV_execute 函数指令实现部分:

case OP_CALL: { // 先执行 ipair 函数,获取迭代器 ipairsaux
    int b = GETARG_B(i);
    int nresults = GETARG_C(i) - 1;
    if (b != 0) L->top = ra+b;  /* else previous instruction set top */
    if (luaD_precall(L, ra, nresults)) {  /* C function? */
        if (nresults >= 0)
            L->top = ci->top;  /* adjust results */
    }
    else {  /* Lua function */
        ci = L->ci;
        goto newframe;  /* restart luaV_execute over new Lua function */
    }
    break;
}
case OP_JMP: {
    ci->u.l.savedpc += GETARG_sBx(i); // 直接跳转到 OP_TFORCALL
    break;
}
case OP_TFORCALL: {
    // 先复制迭代器,不可变状态,控制变量这三个值到栈顶
    TValue *cb = ra + 3;  /* call base */
    *(cb + 1) = *(ra + 1);
    *(cb + 2) = *(ra + 2);
    *cb = *ra;
    L->top = cb + 3;
    // 接着调用迭代器 ipairsaux(不可变状态t,控制变量control)
    luaD_call(L, cb, GETARG_C(i));
    // 更新变量列表
    base = ci->u.l.base;
    L->top = ci->top;
    i = *(ci->u.l.savedpc++);
    ra = RA(i);
    assert(GET_OPCODE(i) == OP_TFORLOOP);
    goto l_tforloop;
}
case OP_TFORLOOP: {
    l_tforloop:
    // 判断第一个变量值是否为 nil,不为 nil,则修改 pc 值,跳到循环体继续执行,为 nil,则 pc 只会+1,跳出循环
    if ((ra+1)->tt_ != LUA_TNIL) {
        *ra = *(ra+1);  /* save control variable ;将第一个变量值保存到控制变量control中,做为下次调用迭代器的参数 */
        ci->u.l.savedpc += GETARG_sBx(i);  /* jump back ;修改 pc 值,跳转到 forbody, 执行里面的代码逻辑*/
    }
    break;
}

对应的堆栈分析,大概如下几张图所示:

大概看懂了泛型 for 循环实现原理后,我们再自己模仿 ipairs 实现一个迭代器:

-- 参数 state 不可变状态,control 控制变量
function my_ipars_iter(state, control)
    control = control + 1
    if state[control] ~= nil then
        return control, state[control]
    else
        return nil
    end
end

-- 返回迭代器,以及 state 不可变状态,control 控制变量初始值
function my_ipars(t)
    return my_ipars_iter, t, 0
end

for i,v in my_ipars({1, "nice", false}) do
    print(i,v)
end

--[[
运行结果:
1       1
2       nice
3       false
]]

此外,稍微修改下,还可以换一种写法:

-- 参数 state 不可变状态,control 控制变量初始值
function my_ipars_iter(state, control)
    control = control + 1
    if state[control] ~= nil then
        return control, state[control]
    else
        return nil
    end
end

-- 返回迭代器
function my_ipars()
    return my_ipars_iter
end

local t = {1, "nice", false}
-- in 后面还可以指定不可变状态,控制变量
for i,v in my_ipars(), t, 0 do
    print(i,v)
end

--[[
运行结果:
1       1
2       nice
3       false
]]

 

posted @ 2024-05-08 21:22  墨色山水  阅读(327)  评论(0)    收藏  举报