性能分析工具
优化之前必须先量化。Lua 提供了基础的性能测量工具:
-- os.clock 测量 CPU 时间
local function benchmark(name, func, iterations)
iterations = iterations or 1
local start = os.clock()
for _ = 1, iterations do
func()
end
local elapsed = os.clock() - start
print(string.format("[%s] %.4f 秒 (%d 次)",
name, elapsed, iterations))
return elapsed
end
-- 使用
benchmark("空循环", function()
for i = 1, 1000000 do end
end)
局部变量 vs 全局变量
局部变量的访问速度远快于全局变量,因为局部变量直接存储在栈上或寄存器中:
-- 慢:每次访问都要查找全局表 _G
local sum = 0
for i = 1, 1000000 do
sum = sum + math.sin(i) -- math 是全局变量
end
-- 快:缓存为局部变量
local sin = math.sin
local sum = 0
for i = 1, 1000000 do
sum = sum + sin(i)
end
在循环中缓存常用函数是 Lua 性能优化的第一法则:
-- 游戏中的热循环
local insert = table.insert
local remove = table.remove
local floor = math.floor
local random = math.random
local min = math.min
local max = math.max
function update_entities(entities)
for i = #entities, 1, -1 do
local e = entities[i]
e.x = e.x + e.vx
e.y = e.y + e.vy
if e.hp <= 0 then
remove(entities, i)
end
end
end
表操作优化
预分配表空间
-- 慢:表不断扩容,触发多次 rehash
local t = {}
for i = 1, 100000 do
t[i] = i
end
-- 快:预分配(Lua 5.4 支持 table.new)
-- 或者用初始化方式
local t = {}
t[100000] = 0 -- 强制扩容到足够大
for i = 1, 100000 do
t[i] = i
end
数组 vs 哈希表
-- 慢:使用字符串键(哈希表)
local config = {}
config["width"] = 800
config["height"] = 600
config["fps"] = 60
-- 快:使用数字索引(数组部分)
local config = {800, 600, 60}
local WIDTH, HEIGHT, FPS = 1, 2, 3
-- 更快:使用局部变量
local width, height, fps = 800, 600, 60
表复用
-- 慢:每次调用创建新表
function get_position_bad(entity)
return {x = entity.x, y = entity.y}
end
-- 快:复用表
local _pos = {x = 0, y = 0}
function get_position_good(entity)
_pos.x = entity.x
_pos.y = entity.y
return _pos
end
-- 最快:使用多返回值
function get_position_best(entity)
return entity.x, entity.y
end
反向遍历删除
-- 慢:正向遍历删除(导致元素移动)
for i = 1, #list do
if should_remove(list[i]) then
table.remove(list, i)
i = i - 1 -- 还需要调整索引
end
end
-- 快:反向遍历删除
for i = #list, 1, -1 do
if should_remove(list[i]) then
table.remove(list, i)
end
end
-- 最快:交换删除(不保持顺序)
local function swap_remove(list, i)
local n = #list
list[i] = list[n]
list[n] = nil
end
字符串优化
字符串拼接
-- 慢:使用 .. 在循环中拼接(每次都创建新字符串)
local s = ""
for i = 1, 10000 do
s = s .. "x" -- O(n²) 复杂度
end
-- 快:使用 table.concat
local parts = {}
for i = 1, 10000 do
parts[i] = "x"
end
local s = table.concat(parts) -- O(n) 复杂度
-- 快:使用 string.rep(重复字符串)
local s = string.rep("x", 10000)
字符串格式化
-- string.format 比多次 .. 更快
local name, level, hp = "Player", 50, 1000
-- 慢
local s1 = name .. " Lv." .. level .. " HP:" .. hp
-- 快
local s2 = string.format("%s Lv.%d HP:%d", name, level, hp)
函数调用优化
减少函数调用开销
-- 慢:函数调用开销大
function add(a, b) return a + b end
local sum = 0
for i = 1, 1000000 do
sum = add(sum, i)
end
-- 快:内联运算
local sum = 0
for i = 1, 1000000 do
sum = sum + i
end
使用多返回值代替表
-- 慢:返回表
function calculate_bad(x, y)
return {
sum = x + y,
product = x * y,
diff = x - y
}
end
-- 快:多返回值
function calculate_good(x, y)
return x + y, x * y, x - y
end
local sum, prod, diff = calculate_good(10, 3)
数学运算优化
-- 位运算代替乘除法(Lua 5.3+)
local x = 100
local doubled = x << 1 -- x * 2
local halved = x >> 1 -- x / 2(整数除法)
local mod8 = x & 7 -- x % 8
-- 整数运算比浮点运算快(Lua 5.3+)
local a = 10 -- 整数
local b = 20 -- 整数
local c = a + b -- 整数加法
-- 避免不必要的类型转换
local n = tonumber("42") -- 有开销
local m = 42 -- 无开销
循环优化
-- 减少循环中的计算
-- 慢
for i = 1, #list do
local factor = math.sqrt(list[i].x^2 + list[i].y^2)
-- ...
end
-- 快
local sqrt = math.sqrt
local len = #list
for i = 1, len do
local e = list[i]
local factor = sqrt(e.x * e.x + e.y * e.y)
-- ...
end
-- 展开小循环
-- 慢
for i = 1, 4 do
result[i] = source[i] * 2
end
-- 快
result[1] = source[1] * 2
result[2] = source[2] * 2
result[3] = source[3] * 2
result[4] = source[4] * 2
内存分配优化
-- 对象池模式
local ObjectPool = {}
ObjectPool.__index = ObjectPool
function ObjectPool.new(create_fn, reset_fn, initial_size)
local pool = setmetatable({
_objects = {},
_create = create_fn,
_reset = reset_fn
}, ObjectPool)
-- 预创建对象
for i = 1, initial_size or 10 do
pool._objects[i] = create_fn()
end
return pool
end
function ObjectPool:acquire()
local n = #self._objects
if n > 0 then
local obj = self._objects[n]
self._objects[n] = nil
return obj
end
return self._create()
end
function ObjectPool:release(obj)
if self._reset then
self._reset(obj)
end
self._objects[#self._objects + 1] = obj
end
-- 使用:游戏中的子弹对象池
local bullet_pool = ObjectPool.new(
function() return {x=0, y=0, vx=0, vy=0, active=false} end,
function(b) b.x=0; b.y=0; b.vx=0; b.vy=0; b.active=false end,
100 -- 预创建 100 个子弹
)
-- 获取子弹
local bullet = bullet_pool:acquire()
bullet.x = 100; bullet.y = 200; bullet.active = true
-- 回收子弹
bullet_pool:release(bullet)
LuaJIT 优化
LuaJIT 可以将 Lua 代码编译为机器码,但需要遵循特定模式:
NYI(Not Yet Implemented)清单
以下特性会阻止 JIT 编译,回退到解释器:
-- 会阻止 JIT 的操作
-- 1. 使用 pairs()(LuaJIT 2.1 已部分支持)
for k, v in pairs(t) do end -- 考虑用 ipairs 或数字循环
-- 2. 使用 coroutine.yield 在 C 函数调用链中
-- 3. 使用 string.format 的某些格式
-- 4. 使用 table.insert/table.remove(部分支持)
-- 5. 使用 FFI 回调
JIT 友好的代码模式
-- 使用 ipairs 或数字循环
for i = 1, #t do
-- JIT 可以优化这个循环
end
-- 使用简单类型
local x = 42 -- 整数
local y = 3.14 -- 浮点数
local s = "hello" -- 字符串
-- 避免类型变化
-- 不好:变量类型在不同迭代中变化
local val = 0 -- number
val = "hello" -- string (类型变化阻止 JIT)
-- 好:保持类型一致
local val_num = 0
local val_str = "hello"
FFI 结构体代替 Lua 表
local ffi = require("ffi")
ffi.cdef[[
typedef struct {
float x, y, z;
} Vec3;
]]
-- LuaJIT 对 FFI 结构体有极好的优化
local function vec3_add(a, b)
return ffi.new("Vec3", a.x + b.x, a.y + b.y, a.z + b.z)
end
-- 使用数组而非多个对象
local positions = ffi.new("Vec3[10000]")
for i = 0, 9999 do
positions[i].x = i
positions[i].y = i * 2
positions[i].z = i * 3
end
性能对比基准
-- 综合基准测试
local function run_benchmarks()
local N = 1000000
-- 1. 全局 vs 局部变量
benchmark("全局变量访问", function()
local sum = 0
for i = 1, N do
sum = sum + math.abs(i)
end
end)
local abs = math.abs
benchmark("局部变量访问", function()
local sum = 0
for i = 1, N do
sum = sum + abs(i)
end
end)
-- 2. 表创建 vs 复用
benchmark("每次创建新表", function()
for i = 1, N do
local t = {x = i, y = i}
end
end)
local _t = {x = 0, y = 0}
benchmark("复用表", function()
for i = 1, N do
_t.x = i
_t.y = i
end
end)
-- 3. 字符串拼接
benchmark("table.concat", function()
local parts = {}
for i = 1, 10000 do
parts[i] = "x"
end
table.concat(parts)
end)
benchmark("string.rep", function()
string.rep("x", 10000)
end)
end
run_benchmarks()
优化清单
在进行 Lua 性能优化时,按照以下优先级逐步检查:
- 第一优先级:将热循环中的全局变量缓存为局部变量
- 第二优先级:减少表创建,使用对象池或复用表
- 第三优先级:使用
table.concat替代字符串拼接 - 第四优先级:使用多返回值替代返回表
- 第五优先级:反向遍历删除,使用 swap_remove
- 第六优先级:考虑 LuaJIT 和 FFI
- 最后手段:将热代码用 C 扩展重写
注意事项
- 优化前必须使用性能分析工具定位瓶颈,不要盲目优化
- 可读性和可维护性同样重要,不要为了微小提升牺牲代码清晰度
- LuaJIT 的性能通常是标准 Lua 的 10-100 倍,优先考虑使用 LuaJIT
- Lua 5.3+ 的整数类型比浮点数运算更快
table.new(Lua 5.4 / LuaJIT) 可以预分配表空间,减少 rehash- GC 调优也是性能优化的一部分,参考垃圾回收相关章节
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。