完整课程入口:996 全套课程体系 | Lua 学习路径 | 幂尔框架 mirs.cn
Lua 源码并不是直接执行的:先由编译器编译成字节码指令,再由虚拟机逐条解释执行。看懂字节码,能让你的优化从"感觉"变成"证据"——哪行代码贵、贵在几次查表还是几次函数调用,字节码一目了然。配合标准库 string.dump 可以导出函数的字节码,再借助 luac -l 或第三方反汇编工具阅读。
Lua 5.3/5.4 的每条指令固定 32 位:一个操作码(OPCODE)加最多三个操作数。指令分为几大家族:LOAD/MOVE 装载常量与寄存器搬移;GETTABUP/GETTABLE 是表访问(读全局变量其实也是查环境表的 GETTABUP);CALL/TAILCALL 函数调用;arith 家族做算术;JUMP/EQ/LT/FORLOOP 控制流程。函数的局部变量存放在虚拟机寄存器里,所以局部变量访问是零开销的寄存器操作,而全局变量访问是一条真实的表查询——这就是"能 local 就 local"的字节码证据。
全局变量比局部变量慢。 for i = 1, 100 do print(x) end 里每次 print、每次 x 都要走全局表查询。热点循环开头先 local print = print; local x = gX 固化成寄存器值,字节码上少掉几十条 GETTABUP。
方法链有代价。 obj.a.b:c() 每个点号都是一条表查询指令。在循环里反复访问的长链,提前取一次存局部变量即可砍掉多余查询。
闭包创建有指令成本。 循环体内定义匿名函数,每轮都会生成新闭包(MOVECLOSURE 系列指令 + 分配),把它挪到循环外只创建一次,逻辑等价且更省。
不需要背指令集,用两次就会上手:第一次,luac -l -l your.lua 看一段怀疑很慢的代码,数一数表查询指令的数量;第二次,改造后再 dump 对比,指令数下降就是优化生效的硬证据。把"字节码审查"纳入热点函数优化的固定流程,团队对性能的讨论就能从玄学进入工程。
当然,别过度:解释执行的整体成本大头永远是算法与 I/O,指令级优化留给被测量证明过的热点,才是它的正确用法。
printusetime(play, on, off)
打印脚本总耗时(微秒),同一段逻辑的指令数与实测耗时往往不成正比
| 参数 | 类型 | 说明 |
|---|---|---|
| play | object | 玩家对象 |
| on | integer | 1=开始技术2=结束计时并打印耗时 |
| off | integer | 2=结束计时 |
printusetime(actor,1)
release_print(msg)
打印消息到控制台,读字节码前先确认到手的确实是目标函数
| 参数 | 类型 | 说明 |
|---|---|---|
| msg | any | 打印内容 |
getconst(actor, varname)
获取常量,引擎暴露的常量值可直接在脚本里读,不必硬编码
| 参数 | 类型 | 说明 |
|---|---|---|
| actor | object | 玩家对象 |
| varname | string | 常量名称支持带尖括号和不带尖括号或$Name |
require(path)
加载文件,不同装载方式生成的字节码并不一致,对比要在同一路径下做
| 参数 | 类型 | 说明 |
|---|---|---|
| path | any | 文件名两种加载接口起始路径不同 |
callscriptex(actor, scriptname, arr)
调用传奇脚本命令,脚本命令与 lua 指令的成本差异,是优化的常见切入点
| 参数 | 类型 | 说明 |
|---|---|---|
| actor | object | 玩家对象(必填参数) |
| scriptname | string | 脚本接口(必填参数) |
| arr | any | 参数1~参数10(必填参数) |