循环里的每一步都在做同样的查找:配置表按名字反复索引、引擎变量反复读取、同一个函数反复调用——结果不变的计算被重复执行。查询外提把循环内不变的计算搬到循环外做一次,省下的是整个循环的重复量。
---改造前:循环内反复查配置与读变量
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 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
实战应用:用在哪里 战士的烈火剑法一击熄火后要等八秒冷却,连招断档感强烈。疾风烈火斩在烈火剑法命中后给一次连击窗口:三秒内再…
实战应用:用在哪里 阵营对抗打到中期容易疲软:打赢没有额外好处,输掉也没有代价。阵营声望体系给阵营战装上荣誉刻度:个人与阵营…
实战应用:用在哪里 拿下沙巴克之后呢?城主除了名字挂在城墙上,对城市毫无影响。城主施政玩法让占领变成治理:城主每周获得市政令…
实战应用:用在哪里 主线路光缆被挖断的深夜,全服掉线 20 分钟,玩家以为游戏倒闭。备用线路方案给接入层配第二条出口:主线路…
实战应用:用在哪里 行会官员由帮主任命的老模式弊端明显:任人唯亲、能上不能下。行会选举接口把官员换届做成投票制:每两个月一次…
实战应用:用在哪里 烈火剑法的伤害加成、冷却时间、下一级提升,全靠玩家记忆或去官网查。技能说明悬浮卡片在鼠标悬停技能图标零点…