前几天写代码lua时,由于涉及到大量的数值逻辑计算,所以性能至关重要。经过一番研究和调试,总结了如下提高lua代码执行效率的方法:

  • 使用缓存

这个不论是在lua,在任何语言的程序中都适用,脚本语言显得尤为突出。使用缓存来优化,提高程序性能是一个很大的主题,这里不再详细论述。只需记得,当使用重复数据(或有重叠)时一定要缓存起来,以供后面使用,而不是每次都重新计算,这样会大大提高效率,尤其是涉及到繁杂的计算时。

  • 尽量不要在返回值中使用table

当我们需要返回多个值的时候,我们会考虑到返回一个table,类似C++里面返回一个结构体(或指针)一样。例如我们经常要判断函数是否执行成功,所以一般除了返回一个想要的结果外,还有返回一个bool值来表明函数是否执行成功。例如:

function f() 
  ...
  local ret = ...
  local ok = ...
  return {Ret = ret, Ok = ok}
end

遗憾的是,这种写法虽然清晰,但是会有一定性能损耗。我做了如下实验,一个函数返回table,一个函数返回多个值,看看这两种方法的执行时间:

function func1()
    return 1, 2
end
function func2()
    return {a = 1, b = 2}
end
local count = 20000000 
for i = 1, count do
    local a, b = func1()        --①
    --local a, b = func2()     --②
end

执行函数①所要的时间为:

 

执行函数②所要的时间为:

 

我们可以看出其差别还是挺大的。

  • 不要使用字符串作为比较对象

我们看如下例子:

function func3(param)
    if param == 1 then
        return 1
    elseif param == 2 then
        return 2
    end
end
function func4(param)
    if param == 'test_first' then
        return 1
    elseif param == 'test_two' then
        return 2
    end
end

local count = 20000000 

local function test3()
    for i = 1, count do
        local p = 1
        local a = func3(p)
    end
end

local function test4()
    for i = 1, count do
        local p = 'first'
        local a = func4(p)
    end
end

test3()
--test4()

他们执行的时间分别是(考虑到杠精,这里多次执行,取平均时间):

 

 

大约提升了10%的执行时间。

  • 不要使用面向对象技巧

在lua中使用面向对象的技巧很是常见,他可以提高代码的可读性,有利于代码的模块化还有封装。但是究竟面向对象技巧会有多少性能损失呢,请看如下代码:

function table.deepcopy(t, nometa)
    local lookup_table = {}
    local function _copy(t,nometa)
        if type(t) ~= "table" then
            return t
        elseif lookup_table[t] then
            return lookup_table[t]
        end
        local new_table = {}
        lookup_table[t] = new_table
        for index, value in pairs(t) do
            new_table[_copy(index)] = _copy(value)
        end
        if not nometa then
           new_table = setmetatable(new_table, getmetatable(t))
        end
        return new_table
    end
    return _copy(t)
end

local Object = {}
function Obj.func() end

function Object:new (data)  
    data = data or {}
    setmetatable(data, {__index = self})   
    return data   
end 

function Object:new2 (data)  
    data = data or {}
    local copy = table.deepcopy(self)
    setmetatable(data, {__index = copy})   
    return data   
end 
 
local count = 2000000 
for i = 1, count do
    --local o = Object
    --local o = Object:new()
    local o = Object:new2()
end

 

上面涉及到两种面向对象的写法,一种是共享函数式的面向对象写法,另一种是数据分离式的面向对象写法。共享函数的面向对象写法不能在table中包含字段,否则会导致字段被所以实例共享,关于他们的区别我会另写一篇文章来讲述。总之尽量不要用到面向对象的方式来生成新的实例,因为他们的效率相差很大,从上图可以看出有几十倍甚至百倍的差距。

注意,我上面讲的的都是比较极端情况下优化代码的方法,比如代码中百万级别的循环。大多时候是不需要这么做的,因为单次运算时间是很短的,不会成为效率瓶颈。况且上面提到的方法有可能使代码变得不那么好读,没有面向对象也使代码逼格不能提升。总之,在实际项目中要灵活选择和使用,不要一味追求效率使代码失去了可读性,更不要放弃优化代码让项目失去价值。

不太影响性能的地方

写了几年的代码,我多多少少都会关注代码的质量,所以我常常会思考各种写法的优劣。代码的效率是一个很重要的指标,所以我会过多的关注代码的性能。在lua的时候,为了考虑到封装性,我常常将函数中的一大段逻辑写成一个内部函数,即闭包函数,而不是一个对等的外部函数。那么他们会有效率上的差别吗?由于这个与具体逻辑关系密切,各种场景下运行的时间各有差异,但是总体来说他们之间的执行效率差距是很小的,在考虑执行时间效率这个因素时可以忽略这一点。

还有在遍历数组类型的table时,用pairs和ipairs执行时间效率差距也是很小的,根据需要随便选一个就好,但是如果遍历map型table时,就只能用pairs了。