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
]]

浙公网安备 33010602011771号