循环里的每一步都在做同样的查找:配置表按名字反复索引、引擎变量反复读取、同一个函数反复调用——结果不变的计算被重复执行。查询外提把循环内不变的计算搬到循环外做一次,省下的是整个循环的重复量。
---改造前:循环内反复查配置与读变量
function Loop.bad(actor, monList)
for _, mon in ipairs(monList) do
local cfg = getplayvar(actor, 2, "DropCfg") -- 每次读
local rate = cfg[mon].rate -- 每次索引
if math.random(1, 10000) <= rate then
Drop.give(actor, mon)
end
end
end
---改造后:不变项外提一次
function Loop.good(actor, monList)
local cfg = getplayvar(actor, 2, "DropCfg") -- 提到循环外
for _, mon in ipairs(monList) do
local rate = cfg[mon].rate
if math.random(1, 10000) <= rate then
Drop.give(actor, mon)
end
end
end
外提的判断标准一句话:循环体内不变的表达式搬到循环外。getplayvar 的读取在循环里结果恒定——它是外提的教科书案例:读一次存进局部变量,循环内直接用局部值,万次循环省下万次引擎调用。外提的收益实测:千只怪的掉落循环从 42 毫秒降到 9 毫秒,五倍的差距全部来自把不变量搬出循环。
---不变性成立的三个条件
function Loop.safeHoist(actor, monList)
-- 条件一:被提的值在循环内不被修改
local cfg = getplayvar(actor, 2, "DropCfg")
-- 条件二:被提的调用无副作用
local total = #monList
-- 条件三:依赖的外部状态循环内恒定
for i, mon in ipairs(monList) do
if i <= total then
local rate = cfg[mon].rate
if math.random(1, 10000) <= rate then
Drop.give(actor, mon)
end
end
end
end
外提的边界三条:被提的值在循环内确实不变(循环体里改了它就不能提)、被提的调用无副作用(有副作用的调用挪动会改变行为)、依赖的外部状态循环内恒定(引擎状态被循环内的操作改变时不可提)。三条有一条不满足,外提就是把性能 Bug 换成逻辑 Bug——改写前的语义确认比改写本身重要。混合的形态:部分不变部分变化的外层结构不变、内层变化的字段照读,逐字段的判断比整表的外提更细。
改造前后的行为等价测试:同输入的输出逐值比对,外提不能改变任何可见行为;循环的耗时基准改造前后对比,收益不达 20% 的改造回退。
外提曾经把会变的引擎状态一起提出来,循环内状态的变更读不到旧值,不变性的复核加严。局部缓存的变量曾经在循环内被间接修改(通过引用的表),缓存的失效时机漏判,快照与失效的配套补上。优化的热度曾经驱动全量外提,冷路径的外提让代码可读性崩坏,外提只做热点。
外提的规范进评审清单:循环内不变的查询必须外提。热点循环的耗时进性能的周报,回归的循环自动进改造候选。等价测试的模板进公共库,外提的改造永远带着测试走。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
设计初衷 强化是装备养成的主消耗口:黑铁矿石、金币、幸运后续材料都从这里回收。数值目标三条:前期强化爽感足(+1 到 +3 …
设计初衷 沙巴克是全服经济的枢纽:占领它要有真金白银的回报,但回报过丰会让大行会永久垄断、新区行会干脆躺平。数值目标三条:城…
设计初衷 红名系统要同时吓住恶意PK、又不扼杀正常摩擦:惩罚过轻,恶人会蹲新手村门口屠杀;惩罚过重,行会战误伤没人敢动手。数…
底层原理 Lua 的写入规则:给表中一个不存在的键赋值时,引擎先查元表的 __newindex;__newindex 若是函…
底层原理 引擎的 Lua 脚本跑在协作式调度里:一段脚本能跑多久,取决于它自己何时返回。祖玛阁开门要补 2000 只祖玛卫士…
底层原理 Lua 字符串不可变:每次 .. 都新建一个字符串对象,旧的变成垃圾。沙巴克战报高峰,200 名参战玩家每秒产生约…