Lua Gems
性能优化
关于性能优化的两条格言:
-
不要优化
-
还是不要优化(仅限专家)
不要在缺乏恰当度量(measurements)时试图去优化软件。编程老手和菜鸟之间的区别不是说老手更善于洞察程序的性能瓶颈,而是老手知道他们并不善于此。
做性能优化离不开度量。优化前度量,可知何处需要优化。优化后度量,可知「优化」是否确实改进了代码。
basic
lua使用基于寄存器的虚拟机,用一个栈来存放它的寄存器。lua中每个active的函数都有一个activation record放在栈里,记录了这个函数使用的寄存器。因此,每个函数都有其自己的寄存器。由于每条指令只有 8 个 bit 用来指定寄存器,每个函数便可以使用多至 250 个寄存器。Lua 的寄存器如此之多,预编译时便能将所有的局部变量存到寄存器中。所以,在 Lua 中访问局部变量是很快的。
-
因此,可以是用局部变量提高性能。例如给多次引用的外部变量(e.g.
tbl.data
)赋予别名。但是这在 LuaJIT 上几乎已经没有了优势。 -
变量创建问题考虑比较跨域性能消耗和创建消耗:循环内创建简单变量比循环外创建多次覆写效率高;循环外创建长table比循环内创建效率高。
-
避免动态编译(
loadfromstring()
等),可以使用闭包等来避免。 -
Short inline expressions can be faster than function calls.
t[#t+1] = 0
is faster thantable.insert(t, 0)
.
table
Lua 实现表的算法颇为巧妙。每个表包含两部分:数组(array)部分和哈希(hash)部分,数组部分保存的项(entry)以整数为键(key),从 1 到某个特定的 n,(稍后会讨论 n 是怎么计算的。)所有其他的项(包括整数键超出范围的)则保存在哈希部分。
-
顾名思义,哈希部分使用哈希算法来保存和查找键值。它使用的是开放寻址(open address)的表,意味着所有的项都直接存在哈希数组里。键值的主索引由哈希函数给出;如果发生冲突(两个键值哈希到相同的位置),这些键值就串成一个链表,链表的每个元素占用数组的一项。
-
当 Lua 想在表中插入一个新的键值而哈希数组已满时,Lua 会做一次重新哈希(rehash)。重新哈希的第一步是决定新的数组部分和哈希部分的大小。所以 Lua 遍历所有的项,并加以计数和分类,然后取一个使数组部分用量过半的最大的 2 的指数值,作为数组部分的大小。而哈希部分的大小则是一个容得下剩余项(即那些不适合放在数组部分的项)的最小的 2 的指数值。
-
Lua 只在重新哈希时才有机会去收缩(shrink)表。
-
创建定长table使用table.new(narray, nhash)提前声明大小可以避免表扩容操作。
-
如果table存在大量索引不存在的key的情况,该table要避免有元表。元表查询性能消耗是普通查询的30倍左右。
string
3R: reduce, reuse, recycle
reduce
reduce是最简单的一种途径。有几种方法可以避免创建对象。
-
很多字符串的处理,都可以通过在现有字符串上使用下标,来避免创建不必要的新字符串。例如,函数
string.find
返回的是给定模式出现的位置,而不是一个与之匹配的字符串。返回下标,就避免了在成功匹配时创建一个新的(子)字符串。若有需要,可以再通过函数string.sub
来获取匹配的子字符串。 -
循环是寻找降低不必要资源创建的好地方。
-
将闭包中的inner函数移到闭包外。
reuse
即使不能避免使用新的对象,也可以通过 reuse来避免创建新的对象。实现reuse的一种特别有效的方法是记忆化(memoizing)。基本想法非常简单:对于一个给定的输入,保存其计算结果,当遇到同样的输入时,程序只需重用之前保存的结果。
-
对字符串来说,重用是没有必要的,因为 Lua 已经替我们这样做了:所有的字符串都是内化的(internalized),因此只要可能就会重用。对表来说,重用就显得卓有成效了。例如在循环内创建表的情况,往往只需简单的改变内容,还是可以在所有的迭代中重用同一个表的。
-
实现重用的一种特别有效的方法是记忆化(memoizing)。基本想法非常简单:对于一个给定的输入,保存其计算结果,当遇到同样的输入时,程序只需重用之前保存的结果。
recycle
Lua 中的大多数recycle都是由垃圾收集器自动完成的。Lua 使用一个增量(incremental)的垃圾收集器,逐步(in small steps)回收(增量地),跟程序一起交错执行。每一步回收多少,跟内存分配成正比:Lua 分配了多少内存,垃圾收集器就做多少相应比例的工作。程序消耗内存越快,收集器尝试回收内存也就越快。
-
如果我们在程序中遵守减少和重用的原则,收集器通常没有太多的事情可做。但是有时候我们不能避免创建大量的垃圾,这时收集器就可能变得任务繁重了。Lua 的垃圾收集器是为一般的程序而设的,对大多数应用来说,它的表现都是相当不错的。但是有时候,某些特殊的应用场景,适当地调整收集器还是可以提高性能的。
-
函数
collectgarbage
提供了这样几种功能:它可以停止和重启收集器,强制进行一次完整的收集,强制执行一步收集(collection step),得到当前内存使用总量,更改两个影响收集效率(pace)的参数。所有这些操作在缺乏内存的程序里都有其用武之地。 -
collectgarbage
的参数对程序的总体性能的影响是很难预测的。收集器越快,其每秒耗费的 CPU 周期显然也就越多;但是另一方面,或许这样能减少程序的内存使用总量,从而减少换页(paging)。只有通过仔细的实验,才能为这些参数找到最佳的值。
LuaJIT
减少无法预知的分支数
有极高可能性偏向一方的条件分支不会影响(95%以上)
-
可以改用无分支的算法替换,如
使用
math.min()
和math.max()
-
使用
bit.*
库
-
使用
FFI
数据结构使用
int32_t
,避免使用uint32_t
类型-
使用
double
,避免使用float
-
不要滥用元方法
-
仅通过
FFI
调用C函数避免调用很小的函数,更好的做法是在Lua重写
-
避免回调函数,使用推送(
read/write
)或者迭代器来代替
-
使用诸如
for i=start,stop,step do ... end
的循环语句数组索引也类似, e.g.
a[i+2].
-
避免使用指针计算
-
展开适度
避免循环量少的内联循环
-
当且仅当循环体有太多指令时展开循环
-
考虑使用模板代替手动展开(如GSL Shell)
-
最好仅在模块(module)内定义并调用函数
-
对于其他模块的函数,如果常用,则缓存为上值(upvalue)
如,
local sin = math.sin
-
对于FFI C的函数不要这么做,缓存名字空间即可,如
local lib = ffi.load("lib")
-
避免自己创建派遣机制,而是改用系统自带的,如元方法
-
不要过度优化,JIT编译器可能已经优化过了
例如,
z = x[a+b] + y[a+b]
的写法是没有问题的,如果手动增加一个临时变量存放a+b
,不仅不会提升性能,还会导致多占用了一个寄存器或者栈空间,得不偿失-
例如,
a[i][j] = a[i][j] * a[i][j+1]
也一样没有问题 -
在使用LuaJIT的时候,这些细小的优化点编译器均帮忙做了更优的实现,程序员更需要关心的是更为宏大的优化,如算法复杂度的优化
-
不同于C/JAVA,变量的声明和定义尽量放在要用的地方,而不是函数开头
-
不要过度使用昂贵或未编译的操作
如
assert(type(x) == "number", "x is a "..mytype(x)")
,该语句最大的问题在于字符串拼接,该操作无论断言是否生效均需要每次操作-
通过
-jv/-jdump
来看output确认是否有不需要的操作
评论
发表评论