通过字节码理解lua的for循环
SH疫情隔离在家,打算把lua和unity热更新相关的问题再升入学习一下。本篇是这个系列的第一篇。一直以来笔者都抱有这样一个观念:lua是个好语言但被滥用了,一门胶水语言在当前的游戏开发中承担了它不该承担的任务。作为一种弱类型语言,并且没有建立足够成熟的生态(包括编辑器、第三方库)的情况下,被作为手游客户端开发的主语言,动辄几万行的业务逻辑。这对项目和开发者来说都不是一个好的选择,只能说是向ios这个傲慢的平台的一种妥协。
如果可以选择的话,我只想说:lua都不写!
先看一段lua代码
local i = 0for i = 1,5 do i = 5 print(i)endprint("after loop")print(i)
这里我们关注两个问题:
1.for循环执行了多少次?
2.afterloop之后,打印出来的i的值是多少?
loop.png
第二个问题比较好理解,由于在for循环内部出现了同名的控制变量,所以外部i实际上在循环内部没有作用域,因此并没有被赋值。
但第一个问题,为什么循环依然执行了5次,而不是在i=5这次赋值之后进入for循环的下一次判断就结束呢?
要解释这个问题,我们就需要探究一下lua代码底层到底是怎么执行的了。
lua程序的运行方式:
1.由lua解释器将lua代码翻译成指令序列
2.由lua虚拟机执行指令序列
(lua解释器和lua虚拟机均由纯粹的C语言实现,要理解更完整的实现细节可以参考lua源代码http://www.lua.org/download.html)
每条lua指令由 操作码+操作数 组成
一条指令使用一个32bit的无符号整数表示,其中低6位表示操作码,操作码定义在lopcodes.h中
如何查看lua代码对应的指令序列
方法一,直接通过luac输出指令序列(这种方法没有对指令的额外解释,对新手来说不太友好)
命令行:luac -l test.lua
方法二,通过ChunkSpy.lua辅助解析
命令行:lua ChunkSpy.lua --source test.lua
(ChunkSpy.lua的获取地址https://github.com/viruscamp/luadec/tree/master/ChunkSpy)
通过方法二我们可以得到上面那段代码对应的lua指令序列
1. local i = 02.003B01000000 loadk 0 0 ; R0 := K0(=0)3. for i = 1,5 do4.003F41400000 loadk 1 1 ; R1 := K1(=1)5.004381800000 loadk 2 2 ; R2 := K2(=5)6.0047C1400000 loadk 3 1 ; R3 := K1(=1)7.004B68C00080 forprep 1 4 ; R1 -= R3; pc+=4 (goto )8. i = 59.004F01810000 loadk 4 2 ; R4 := K2(=5)10. print(i)11.005346C14000 gettabup5 0 259; R5 := U0(=_ENV)12.005780010002 move 6 4 ; R6 := R413.005B64410001 call 5 2 1 ;:= R5(R6)14. end15.005F6780FE7F forloop 1 -5 ; R1 += R3; if R1 <= R2 then { R4 := R1; pc+=-5 (goto ) }16. print("after loop")17.006346C04000 gettabup1 0 259; R1 := U0(=_ENV)18.006781000100 loadk 2 4 ; R2 := K4(="after loop")19.006B64400001 call 1 2 1 ;:= R1(R2)20. print(i)21.006F46C04000 gettabup1 0 259; R1 := U0(=_ENV)22.007380000000 move 2 0 ; R2 := R023.007764400001 call 1 2 1 ;:= R1(R2)24.007B26008000 return 0 1 ; return
重点关注这几行:
4-7行
9行
15行
我们解释一下其中几个关键的命令
loadk A B --R(A) := K(B)加载常量操作码,将B所指的常量加载到A所指的寄存器中move A B --R(A) := R(B)赋值操作码,将寄存器B中的值拷贝到寄存器A中。 forprep A B --R(A) -= R(A+2); PC += B初始化数字for循环forloop A B --R(A) += R(A+2); if R(A) <= R(A+1) then { R(A+3) := R(A); PC + =-B}执行数字for循环的一次迭代
我们看一下《lua虚拟机指令简明手册》中关于for循环指令的说明:
数字for循环要求栈上的4个寄存器,每个寄存器都必须是数值。R(A)持有初始值并作为内部循环变量(内部索引);R(A+1)是界限;R(A+2)是步进值;R(A+3)是局部于 for 块的实际循环变量(外部索引)
我们注意这里,for循环用于循环移步和条件判断的始终是R(A)寄存器中的值(对应上面那段代码中的R1),而for循环内部执行段中使用的变量则是寄存器R(A+3)中的值(对应R4),并且每次重新进入循环体之前,R4还会重新被赋值为R1中的值(if R1 <= R2 then { R4 := R1} )
并且《lua虚拟机指令简明手册》特别强调了lua的for循环实现是和传统的测试+跳转的循环方式是不同的。
那么我们再来看一下所谓的“传统的测试和跳转”是怎样的执行过程。以大家最熟悉的C语言为例:
int main(){ for (int i = 0; i < 10; i++) { i = 10; } return 0;}
我们查看对应的汇编代码:
1. for (int i = 0; i < 10; i++)2.007C17C8mov dword ptr ,0 3.007C17CFjmp main+3Ah (07C17DAh) 4.007C17D1mov eax,dword ptr 5.007C17D4add eax,1 6.007C17D7mov dword ptr ,eax 7.007C17DAcmp dword ptr ,0Ah 8.007C17DEjge main+49h (07C17E9h) 9. {10. i = 10;11.007C17E0mov dword ptr ,0Ah 12. }007C17E7jmp main+31h (07C17D1h)
注意到这里所有关于控制变量i的操作都对应于dword ptr
这就是传统的力量!
看到这里,我真是要diss一下lua的for循环设计,整这些花里胡哨的特性有啥用啊?你不会比C语言之父还懂编程吧?
页:
[1]