Lua性能优化实战指南

掌握Lua性能优化核心技巧,包括局部变量、表操作、字符串处理和LuaJIT优化等实战策略。

性能分析工具

优化之前必须先量化。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 调优也是性能优化的一部分,参考垃圾回收相关章节

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页