(本篇所有测试用例均在 lua5.3.4 环境下测得)

1、Table

原理:

1、Lua 实现表的算法颇为巧妙。每个表包含两部分:数组(array)部分和哈希(hash)部分,数组部分保存的项(entry)以整数为键(key),从 1 到某个特定的 n,(稍后会讨论 n 是怎么计算的。)所有其他的项(包括整数键超出范围的)则保存在哈希部分。顾名思义,哈希部分使用哈希算法来保存和查找键值。它使用的是开放寻址(openaddress)的表,意味着所有的项都直接存在哈希数组里。键值的主索引由哈希函数给出;如果发生冲突(两个键值哈希到相同的位置),这些键值就串成一个链表,链表的每个元素占用数组的一项。

2、执行扩容的过程叫做rehash,每次rehash时,会遍历整个table的数组部分和哈希表部分,统计其中有效的键值对,大小不够,则会扩容,扩容后的大小为2的整数幂次方,且保证rehash操作后整个数组部分的使用率大于50%。每次rehash都很耗时,使用table,我们应该尽量减少rehash。

优化:

1、初始化优化,减少rehash的次数.

local param = {};param.type = 1;param.id = 1; param.name = "lua"; 
-- 优化成
local param = {type= 1, id = 1,name = "lua"};

测试:

local cycleTimes = 9999999
local time1 = os.clock()
local mytable = {}
for i=1,cycleTimes do
    local info = {}
    info.a = i
    info.b = i+1
    info.c = i*2
    table.insert(mytable, info)
end

local time2 = os.clock()
print("第一种情况用时:" .. time2-time1 .. "s")

mytable = {}
for i=1,cycleTimes do
    local info = {
        a = i,
        b = i+1,
        c = i*2,
    }
    table.insert(mytable, info)
end
print("第二种情况用时:" .. os.clock()-time2 .. "s")

结果:

第一种情况用时:8.554s
第二种情况用时:6.724s

从测试结果可以看出,优化后的效率提升20%左右

2、扩容

local a = {}
for i = 1, 3 do
 a[i] = true
end

--优化为
local a = {0,0,0}
for i = 1, 3 do
 a[i] = true
end

测试:

local cycleTimes = 9999999
local myTable = {}
local time1 = os.clock()
for i=1,cycleTimes do
    myTable[i] = {}
    for ii = 1, 3 do
        myTable[i][ii] = true
    end
end

local time2 = os.clock()
print("第一种情况用时:" .. time2-time1 .. "s")

myTable = {}
for i=1,cycleTimes do
    myTable[i] = {0, 0, 0}
    for ii = 1, 3 do
        myTable[i][ii] = true
    end
end
print("第二种情况用时:" .. os.clock()-time2 .. "s")

测试结果:

第一种情况用时:5.876s
第二种情况用时:2.572s

从测试结果可以看出,优化后的效率提升55%左右

注:扩容在实际项目中有诸多限制,可根据实际情况采取此类优化

3、数组插入(特定)

local a = {}
table.insert(a, 1)

-- 优化为
local a = {}
a[#a] = 1

 测试:

local cycleTimes = 999999
local time1 = os.clock()
local tmp = {}
for i=1,cycleTimes do
    table.insert(tmp, i)
end

local time2 = os.clock()
print("第一种情况用时:" .. time2-time1 .. "s")
tmp = {}
for i=1,cycleTimes do
   tmp[#tmp+1] = i
end
print("第二种情况用时:" .. os.clock()-time2 .. "s")

 测试结果:

第一种情况用时:0.192s
第二种情况用时:0.116s

 从测试结果可以看出,优化后的效率提升40%左右

4、ipairs遍历

使用#tb替代ipairs

测试:

local allNum = 999999
local testTb = {}

for i=1,allNum do
    table.insert(testTb, i)
end

-- 1:ipairs
local time1 = os.clock()
for i,v in ipairs(testTb) do
    -- print(i,v)
end

local time2 = os.clock()

-- 2:#testTb
for i=1,#testTb do
    -- print(i)
end
local time3 = os.clock()

print("第一次 ipairs 用时:", time2 - time1)
print("第二次 #testTb 用时:", time3 - time2)

测试结果:

第一次 ipairs 用时:	0.259
第二次 #testTb 用时:	0.011

 从测试结果可以看出,#testTb的效率大约是ipairs的25倍

2、字符串

原理

1、Lua 的字符串都是内化的(internalized);这意味着字符串在 Lua 中都只有一份拷贝。每当一个新字符串出现时,Lua 会先检查这个字符串是否已经有一份拷贝,如果有,就重用这份拷贝。内化(internalization)使字符串比较及表索引这样的操作变得非常快,但是字符串的创建会变慢。

2、Lua 的字符串变量从来不会包含字符串本身,包含的只是字符串的引用。这种实现加快了某些字符串操作。

-- 简单的说lua维护了一个table存放了所有的字符串。
-- 任何新创建的字符串都会先hash去table查找一下,有的话直接返回字符串的引用。
-- 没有的话创建个新的字符串放入table,返回新的字符串的引用。

--引用列子
local value = "a" 
value  = value .."b"
print(value )  -- 输出 'ab'
-- 现在 lua string这个大table里,就有了  'a'  和 'ab' 两个字符串了。value 实际引用的是 'ab'。

--字符串的连接列子
-- 'x' .. 'y' .. 'z'
-- 这种就是 x找一回,xy找一回,xyz找一回。
-- 生成3个串 找3回最后table里有了'x','xy','xyz' 三个字符串了。

优化

1、使用运算符 ' .. ',每次拼接都需要申请新的空间,旧的result对应的空间会在某时刻被Lua的垃圾回收期GC,且随着result不断增长,越往后会开辟更多新的空间,并进行拷贝操作,产生更多需要被GC的空间,所以性能降低。

2、使用table.concat (table [, sep [, start [, end]]])函数,table.concat 底层拼接字符串的方式也是使用运算符.. ,但是其使用算法减少了使用运算符..的次数,减少了GC,从而提高效率。

测试:

local testTb = {"aa", "bb", "cc", "dd"}
local cycleTimes = 99999
local time1 = os.clock()
local a, b, c, d = testTb[1], testTb[2], testTb[3], testTb[4]
local tmp = ""
for i=1,cycleTimes do
    tmp = tmp .. a .. b .. c .. d
end

local time2 = os.clock()
print("第一种情况用时:" .. time2-time1 .. "s")
tmp = ""
local forTb = {}
for i=1,cycleTimes do
   for k,v in ipairs(testTb) do
       table.insert(forTb, v)
   end
end
time2 = os.clock()  -- 注释此行即为算上表构建时间后的结果
tmp = table.concat(forTb, "")
print("第二种情况用时:" .. os.clock()-time2 .. "s")

测试结果:

第一种情况用时:6.777s
第二种情况用时:0.015s

从测试结果可以看出,优化后的效率提升高达99.8%左右

注意:此处只计算字符串连接的时间代价,并未计算构建 table.concat 所需 table 表的时间

下面是算上table表构建时间的结果 (测试用例:将上面程序中的第-3行注释即可)

测试结果:

第一种情况用时:6.155s
第二种情况用时:0.109s

从测试结果可以看出,此时的效率提升也高达98.2%左右

3、局部变量

原理

Lua 使用了一个基于寄存器的虚拟机。这些「寄存器」跟 CPU 中真实的寄存器并无关联,因为这种关联既无可移植性,也受限于可用的寄存器数量。Lua 使用一个栈(由一个数组加上一些索引实现)来存放它的寄存器。每个活动的(active)函数都有一份活动记录(activation record),活动记录占用栈的一小块,存放着这个函数对应的寄存器。因此,每个函数都有其自己的寄存器。由于每条指令只有 8 个bit 用来指定寄存器,每个函数便可以使用多至 250 个寄存器注*1

Lua 的寄存器如此之多,预编译时便能将所有的局部变量存到寄存器中。所以,在 Lua 中访问局部变量是很快的。

注*1

a.local变量过多也会有堆栈溢出的问题,一段过程下最多拥有200个local变量,且do end不算。如果超过199,则会报出main function has more than 200 local variables的错误。当然这里说的是一段过程,所以函数是另算的,同样一个函数的过程最多也不能超过200个local变量(调用函数则算转入下一个过程了)。

b.模块级local变量暂无限制,但是也要考虑到热更新方面的问题:若是选择使用模块级local变量去存储模块的数据,那么在热更新方面的处理将会变得十分麻烦。从这点考虑的话,模块级local变量最好只是用于引用别的模块为妙。

优化

1、高频调用类优化

local x = math.sin(i) 
--优化成
local sin = math.sin
local x = sin(i)

测试:

local cycleTimes = 9999999
local MATH_MIN = math.min
local time1 = os.clock()
for i=1,cycleTimes do
    local tmp = math.min(cycleTimes, i)
end

local time2 = os.clock()
print("第一种情况用时:" .. time2-time1 .. "s")
for i=1,cycleTimes do
    local tmp = MATH_MIN(cycleTimes, i)
end
print("第二种情况用时:" .. os.clock()-time2 .. "s")

测试结果:

第一种情况用时:0.418s
第二种情况用时:0.285s

从测试结果可以看出,优化后的效率提升30%左右

4、元表与元方法

5、函数

4和5 查看此链接:Lua 性能,内存优化

6、模块内部写法优化

Lua - 模块内部写法优化