Lua是一门以其性能著称的脚本语言,被普遍应用在不少方面,尤为是游戏。像《魔兽世界》的插件,手机游戏《大掌门》《神曲》《迷失之地》等都是用Lua来写的逻辑。html
因此大部分时候咱们不须要去考虑性能问题。Knuth有句名言:“过早优化是万恶之源”。其意思就是过早优化是没必要要的,会浪费大量时间,并且容易致使代码混乱。程序员
因此一个好的程序员在考虑优化性能前必须问本身两个问题:“个人程序真的须要优化吗?”。若是答案为是,那么再问本身:“优化哪一个部分?”。算法
咱们不能靠臆想和凭空猜想来决定优化哪一个部分,代码的运行效率必须是可测量的。咱们须要借助于分析器来测定性能的瓶颈,而后着手优化。优化后,咱们仍然要借助于分析器来测量所作的优化是否真的有效。编程
我认为最好的方式是在首次编写的时候按照最佳实践去写出高性能的代码,而不是编写了一堆垃圾代码后,再考虑优化。相信工做后你们都会对过后的优化的繁琐都深有体会。数组
一旦你决定编写高性能的Lua代码,下文将会指出在Lua中哪些代码是能够优化的,哪些代码会是运行缓慢的,而后怎么去优化它们。缓存
在代码运行前,Lua会把源码预编译成一种中间码,相似于Java的虚拟机。这种格式而后会经过C的解释器进行解释,整个过程其实就是经过一个while
循环,里面有不少的switch...case
语句,一个case
对应一条指令来解析。数据结构
自Lua 5.0以后,Lua采用了一种相似于寄存器的虚拟机模式。Lua用栈来储存其寄存器。每个活动的函数,Lua都会其分配一个栈,这个栈用来储存函数里的活动记录。每个函数的栈均可以储存至多250个寄存器,由于栈的长度是用8个比特表示的。app
有了这么多的寄存器,Lua的预编译器能把全部的local变量储存在其中。这就使得Lua在获取local变量时其效率十分的高。编辑器
举个栗子: 假设a和b为local变量,a = a + b
的预编译会产生一条指令:函数
0 1 |
;a是寄存器0 b是寄存器1 ADD 0 0 1 |
可是若a和b都没有声明为local变量,则预编译会产生以下指令:
0 1 2 3 |
GETGLOBAL 0 0 ;get a GETGLOBAL 1 1 ;get b ADD 0 0 1 ;do add SETGLOBAL 0 0 ;set a |
因此你懂的:在写Lua代码时,你应该尽可能使用local变量。
如下是几个对比测试,你能够复制代码到你的编辑器中,进行测试。
0 1 2 3 4 5 |
a = os.clock() for i = 1,10000000 do local x = math.sin(i) end b = os.clock() print(b-a) --1.113454 |
把math.sin
赋给local变量sin
:
0 1 2 3 4 5 6 |
a = os.clock() local sin = math.sin for i = 1,10000000 do local x = sin(i) end b = os.clock() print(b-a) --0.75951 |
直接使用math.sin
,耗时1.11秒;使用local变量sin
来保存math.sin
,耗时0.76秒。能够得到30%的效率提高!
表在Lua中使用十分频繁,由于表几乎代替了Lua的全部容器。因此快速了解一下Lua底层是如何实现表,对咱们编写Lua代码是有好处的。
Lua的表分为两个部分:数组(array)部分和哈希(hash)部分。数组部分包含全部从1到n的整数键,其余的全部键都储存在哈希部分中。
哈希部分其实就是一个哈希表,哈希表本质是一个数组,它利用哈希算法将键转化为数组下标,若下标有冲突(即同一个下标对应了两个不一样的键),则它会将冲突的下标上建立一个链表,将不一样的键串在这个链表上,这种解决冲突的方法叫作:链地址法。
当咱们把一个新键值赋给表时,若数组和哈希表已经满了,则会触发一个再哈希(rehash)。再哈希的代价是高昂的。首先会在内存中分配一个新的长度的数组,而后将全部记录再所有哈希一遍,将原来的记录转移到新数组中。新哈希表的长度是最接近于全部元素数目的2的乘方。
当建立一个空表时,数组和哈希部分的长度都将初始化为0,即不会为它们初始化任何数组。让咱们来看下执行下面这段代码时在Lua中发生了什么:
0 1 2 3 |
local a = {} for i=1,3 do a[i] = true end |
最开始,Lua建立了一个空表a,在第一次迭代中,a[1] = true
触发了一次rehash,Lua将数组部分的长度设置为2^0
,即1,哈希部分仍为空。在第二次迭代中,a[2] = true
再次触发了rehash,将数组部分长度设为2^1
,即2。最后一次迭代,又触发了一次rehash,将数组部分长度设为2^2
,即4。
下面这段代码:
0 1 |
a = {} a.x = 1; a.y = 2; a.z = 3 |
与上一段代码相似,只是其触发了三次表中哈希部分的rehash而已。
只有三个元素的表,会执行三次rehash;然而有一百万个元素的表仅仅只会执行20次rehash而已,由于2^20 = 1048576 > 1000000
。可是,若是你建立了很是多的长度很小的表(好比坐标点:point = {x=0,y=0}
),这可能会形成巨大的影响。
若是你有不少很是多的很小的表须要建立时,你能够将其预先填充以免rehash。好比:{true,true,true}
,Lua知道这个表有三个元素,因此Lua直接建立了三个元素长度的数组。相似的,{x=1, y=2, z=3}
,Lua会在其哈希部分中建立长度为4的数组。
如下代码执行时间为1.53秒:
0 1 2 3 4 5 6 |
a = os.clock() for i = 1,2000000 do local a = {} a[1] = 1; a[2] = 2; a[3] = 3 end b = os.clock() print(b-a) --1.528293 |
若是咱们在建立表的时候就填充好它的大小,则只须要0.75秒,一倍的效率提高!
0 1 2 3 4 5 6 |
a = os.clock() for i = 1,2000000 do local a = {1,1,1} a[1] = 1; a[2] = 2; a[3] = 3 end b = os.clock() print(b-a) --0.746453 |
因此,当须要建立很是多的小size的表时,应预先填充好表的大小。
与其余主流脚本语言不一样的是,Lua在实现字符串类型有两方面不一样。
第一,全部的字符串在Lua中都只储存一份拷贝。当新字符串出现时,Lua检查是否有其相同的拷贝,若没有则建立它,不然,指向这个拷贝。这可使得字符串比较和表索引变得至关的快,由于比较字符串只须要检查引用是否一致便可;可是这也下降了建立字符串时的效率,由于Lua须要去查找比较一遍。
第二,全部的字符串变量,只保存字符串引用,而不保存它的buffer。这使得字符串的赋值变得十分高效。例如在Perl中,$x = $y
,会将$y的buffer整个的复制到$x的buffer中,当字符串很长时,这个操做的代价将十分昂贵。而在Lua,一样的赋值,只复制引用,十分的高效。
可是只保存引用会下降在字符串链接时的速度。在Perl中,$s = $s . 'x'
和$s .= 'x'
的效率差距惊人。前者,将会获取整个$s的拷贝,并将’x’添加到它的末尾;然后者,将直接将’x’插入到$x的buffer末尾。
因为后者不须要进行拷贝,因此其效率和$s的长度无关,由于十分高效。
在Lua中,并不支持第二种更快的操做。如下代码将花费6.65秒:
0 1 2 3 4 5 6 |
a = os.clock() local s = '' for i = 1,300000 do s = s .. 'a' end b = os.clock() print(b-a) --6.649481 |
咱们能够用table来模拟buffer,下面的代码只需花费0.72秒,9倍多的效率提高:
0 1 2 3 4 5 6 7 8 |
a = os.clock() local s = '' local t = {} for i = 1,300000 do t[#t + 1] = 'a' end s = table.concat( t, '') b = os.clock() print(b-a) --0.07178 |
因此:在大字符串链接中,咱们应避免..
。应用table来模拟buffer,而后concat获得最终字符串。
3R原则(the rules of 3R)是:减量化(reducing),再利用(reusing)和再循环(recycling)三种原则的简称。
3R原则本是循环经济和环保的原则,可是其一样适用于Lua。
有许多办法可以避免建立新对象和节约内存。例如:若是你的程序中使用了太多的表,你能够考虑换一种数据结构来表示。
举个栗子。 假设你的程序中有多边形这个类型,你用一个表来储存多边形的顶点:
0 1 2 3 4 5 |
polyline = { { x = 1.1, y = 2.9 }, { x = 1.1, y = 3.7 }, { x = 4.6, y = 5.2 }, ... } |
以上的数据结构十分天然,便于理解。可是每个顶点都须要一个哈希部分来储存。若是放置在数组部分中,则会减小内存的占用:
0 1 2 3 4 5 |
polyline = { { 1.1, 2.9 }, { 1.1, 3.7 }, { 4.6, 5.2 }, ... } |
一百万个顶点时,内存将会由153.3MB减小到107.6MB,可是代价是代码的可读性下降了。
最变态的方法是:
0 1 2 3 |
polyline = { x = {1.1, 1.1, 4.6, ...}, y = {2.9, 3.7, 5.2, ...} } |
一百万个顶点,内存将只占用32MB,至关于原来的1/5。你须要在性能和代码可读性之间作出取舍。
在循环中,咱们更须要注意实例的建立。
0 1 2 3 4 |
for i=1,n do local t = {1,2,3,'hi'} --执行逻辑,但t不更改 ... end |
咱们应该把在循环中不变的东西放到循环外来建立:
0 1 2 3 4 |
local t = {1,2,3,'hi'} for i=1,n do --执行逻辑,但t不更改 ... end |
若是没法避免建立新对象,咱们须要考虑重用旧对象。
考虑下面这段代码:
0 1 2 3 |
local t = {} for i = 1970, 2000 do t[i] = os.time({year = i, month = 6, day = 14}) end |
在每次循环迭代中,都会建立一个新表{year = i, month = 6, day = 14}
,可是只有year
是变量。
下面这段代码重用了表:
0 1 2 3 4 5 |
local t = {} local aux = {year = nil, month = 6, day = 14} for i = 1970, 2000 do aux.year = i; t[i] = os.time(aux) end |
另外一种方式的重用,则是在于缓存以前计算的内容,以免后续的重复计算。后续遇到相同的状况时,则能够直接查表取出。这种方式实际就是动态规划效率高的缘由所在,其本质是用空间换时间。
Lua自带垃圾回收器,因此咱们通常不须要考虑垃圾回收的问题。
了解Lua的垃圾回收能使得咱们编程的自由度更大。
Lua的垃圾回收器是一个增量运行的机制。即回收分红许多小步骤(增量的)来进行。
频繁的垃圾回收可能会下降程序的运行效率。
咱们能够经过Lua的collectgarbage
函数来控制垃圾回收器。
collectgarbage
函数提供了多项功能:中止垃圾回收,重启垃圾回收,强制执行一次回收循环,强制执行一步垃圾回收,获取Lua占用的内存,以及两个影响垃圾回收频率和步幅的参数。
对于批处理的Lua程序来讲,中止垃圾回收collectgarbage("stop")
会提升效率,由于批处理程序在结束时,内存将所有被释放。
对于垃圾回收器的步幅来讲,实际上很难一律而论。更快幅度的垃圾回收会消耗更多CPU,但会释放更多内存,从而也下降了CPU的分页时间。只有当心的试验,咱们才知道哪一种方式更适合。
咱们应该在写代码时,按照高标准去写,尽可能避免在过后进行优化。
若是真的有性能问题,咱们须要用工具量化效率,找到瓶颈,而后针对其优化。固然优化事后须要再次测量,查看是否优化成功。
在优化中,咱们会面临不少选择:代码可读性和运行效率,CPU换内存,内存换CPU等等。须要根据实际状况进行不断试验,来找到最终的平衡点。
最后,有两个终极武器:
第1、使用LuaJIT,LuaJIT可使你在不修改代码的状况下得到平均约5倍的加速。查看LuaJIT在x86/x64下的性能提高比。
第2、将瓶颈部分用C/C++来写。由于Lua和C的天生近亲关系,使得Lua和C能够混合编程。可是C和Lua之间的通信会抵消掉一部分C带来的优点。
注意:这二者并非兼容的,你用C改写的Lua代码越多,LuaJIT所带来的优化幅度就越小。
这篇文章是基于Lua语言的创造者Roberto Ierusalimschy在Lua Programming Gems 中的Lua Performance Tips翻译改写而来。本文没有直译,作了许多删节,能够视为一份笔记。
感谢Roberto在Lua上的辛勤劳动和付出!