lua使用优化

这里记录下一些lua使用优化操作。

堆栈溢出

我们在运行lua的时候,有可能遇到这样一种报错 “stack overflow”,先看看下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
function func_r(a)
a = a + 1
if a > 100000 then
print(a)
else
func_r(a)
end
return a
end

x = 1
func_r(x)

语法上的确没有任何问题,但在执行的时候就会出现stack overflow的报错。是什么原因导致堆栈溢出呢?这个就要追究到Lua源码:Lua虚拟机会对堆栈进行一系列的检查(函数:luaL_checkstack),错误类型就有:

1
2
3
4
5
6
“too many arguments”,
“assume array is smaller than 2^40 “,
“string slice too long”,
“too many captures”,
“too many arguments to script”,
“too many nested functions”

例如,上面的代码就属于递归嵌套次数太多,默认限制20000。

不支持边遍历表边删除表中字段

当在遍历表的时候为不存在的字段赋值时,next的遍历顺序是未知的,然而,你可以在遍历时修改已有的字段,或者,你可以删除已经存在的字段。

使用局部变量local

这是最基础也是最有用的策略,虽然使用全局变量并不能完全避免,但还是应该尽量避免,取而代之使用局部变量即local。这里的局部变量也包括函数function,因为在Lua里函数也是一个变量。局部变量的存取会更快,而且在生命周期之外就会释放掉。

尾调用消除

Lua中的函数调用有两种,一种是标准的函数调用,它会需要生成新的一层调用栈,执行函数流程,然后弹出调用栈返回。另一种叫做尾调用,它是对标准函数调用的优化。尾调用不生成新的调用栈,而不复用当前的。在大多数函数式编程语言中,都需要对尾调用做特别优化。因为函数式语言特别依赖函数的层层调用,甚至用尾调用的方式来做循环。传统方式每次函数调用都需要生成新的栈帧,容易造成栈溢出。 尾调用可以看作C中的goto

当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”。

1
2
3
4
--尾调用
function f(x) return g(x) end
--不是尾调用
function f(x) g(x) end

也就是说,当f调用完g之后就再无其他事情可做了。因此在这种情况中,程序就不需要返回那个“尾调用”所在的函数了。所以在“尾调用”之后,程序也不需要保存任何关于该函数的栈信息了。当g返回时,执行控制权可以直接返回到调用f的那个点上。使得在进行“尾调用”时不耗费任何栈空间。将这种实现称为支持“尾调用消除”。

传参时,少用…

…用于可变参数。如果在传参数过多时,为了方便使用…,会降低代码可读性(寻找参数)。

少用字符串连接操作符

lua字符串的实现可知,运用字符串连接操作符每一次都会生成一个新的字符串。可使用table来模拟字符串缓冲区,避免了大量使用连接操作符。table.concat()

table使用预填充技术

Lua解释器背着我们会对表进行重新散列的动作, lua Table新增元素有详细解释。一个整数的key在同一个表中不同的阶段可能被分配到数组或者散列桶部分。
而这个操作的代价是挺大的。
如下面的代码:

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

最开始,Lua创建了一个空表a。在第一次迭代中,a[1] true触发了一次重新散列操作,Lua将数组部分的长度设置为2^0,即1,散列表部分仍为空。在第二次迭代中,a[2]为true再次触发了重新散列操作,将数组部分长度设为2^1,即2。最后一次迭代又触发了一次重新散列操作,将数组部分长度设为2^2,即4。
只有三个元素的表会执行三次重新散列操作,然而有100万个元素的表仅仅只会执行20次重新散列操作而已,因为2^20 = 1048576 > 1000000。 但是,如果创建了非常多的长度很小的表(比如坐标点: point = {x=0, y=0}),这可能会造成巨大的影响。所以,当需要创建非常多的小表时,应预先填充好表的大小,减少解释器被动地进行重新散列操作的过程。
如果你有很多很小的表需要创建,就可以预先填充以避免重新散列操作。比如:{true, true,true},Lua知道这个表有3个元素,所以直接创建了3个元素的数组。 类似地,{ X=l, y=2, z=3,},Lua会在其散列表部分中创建长度为3的数组。
所以,上述代码使用了预填充技术为:

1
2
3
4
local a = {123}
for i=l,3 do
a[i] = true
end

限制require到别的模块

手工编写服务表,防止require到别的服务的lua模块
REQUIRE_CHECK_LIST = {
[“A”] = true,
[“B”] = true,
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local realrequire = require
local loaded = package.loaded
require = function ( moduleName )
local loadedModule = loaded[moduleName]
if loadedModule ~= nil then
return loadedModule
else
local header = moduleName:match("([^%.]*)%..*")
if REQUIRE_CHECK_LIST[header] then
print(try to require others service)
end
return realrequire(moduleName)
end
end

将全局函数首先加载到局部变量中

从局部变盘和全局变量的获取来看,如果针对的是全局变量,那么会比局部变量额外多一条GETGLOBAL指令,用于将这个全局变量加载到当前函数栈中。因此,一个经常使用的全局变量,可以优化为首先加载到一个局部变量中,再针对这个局部变量进行使用。

1
2
3
for i = 1,10000000 do
local x = math.sin(i)
end

使用局部变量优化:

1
2
3
4
local sin = math.sin
for i = 1,10000000 do
local x = sin(i)
end

En nombrando al ruin de Roma , luego asoma. nran a porfía.