循环里的每一步都在做同样的查找:配置表按名字反复索引、引擎变量反复读取、同一个函数反复调用——结果不变的计算被重复执行。查询外提把循环内不变的计算搬到循环外做一次,省下的是整个循环的重复量。
---改造前:循环内反复查配置与读变量
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 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
实战应用:用在哪里 版本更新上线后突然下架了某件热销商品,但更新公告里没有提到这件事。玩家发现后截图传播,两小时内贴吧帖子超…
实战应用:用在哪里 技能升级后缺少视觉与听觉反馈,玩家不确定升级是否成功。技能升级反馈在升级成功的瞬间展示金色光效、播放升级…
实战应用:用在哪里 点击其他玩家的角色时默认弹出查看面板,但在战斗中玩家希望点击即攻击。双击检测在单击查看的基础上增加双击攻…
实战应用:用在哪里 灵宠在战斗中作用有限,玩家养了宠物但缺少日常互动。灵宠派遣让宠物成为玩家的探索伙伴:派遣灵宠外出探索获取…
实战应用:用在哪里 宠物养成线只有喂食和升级,缺少宠物之间的对抗玩法。宠物竞技让两只宠物在专属竞技场进行自动对战,玩家通过调…
实战应用:用在哪里 测试环境直接复制生产库会把真实玩家数据暴露给开发和测试人员,存在隐私泄露风险。脱敏测试库的搭建分三步:从…