前言Lua是一门以性能着称的脚本语言,在很多方面都有广泛的应用,尤其是游戏。像《魔兽世界》的插件,手游《大掌门》《神曲》《迷失之地》等都是Lua写的逻辑。所以大多数时候我们不需要考虑性能问题。Knuth有一句名言:“过早的优化是万恶之源”。这意味着过早的优化是不必要的,会浪费很多时间,而且往往会使代码混乱。所以一个好的程序员在考虑优化性能之前一定要问自己两个问题:“我的程序真的需要优化吗?”。如果答案是肯定的,那么问问自己:“要优化哪一部分?”。我们不能依靠猜测和猜测来决定优化哪个部分。代码的运行效率必须是可衡量的。我们需要分析器的帮助来确定性能瓶颈,然后开始优化。优化之后,我们还是要用profiler来衡量我们做的优化是否真的有效。我觉得最好的办法就是在写最好的时候就按照最佳实践写出高性能的代码,而不是写了一堆垃圾代码再考虑优化。相信大家在工作之余都会对繁琐的优化深有体会。一旦你决定编写高性能的Lua代码,下面将指出Lua中哪些代码可以优化,哪些代码运行缓慢,以及如何优化它们。在使用本地运行代码之前,Lua会将源代码预编译成中间代码,类似于Java虚拟机。然后C解释器将解释此格式。整个过程其实就是通过一个while循环,里面包含了很多switch...case语句,一个case对应一条指令去解析。从Lua5.0开始,Lua采用了类似于寄存器的虚拟机模式。Lua使用栈来存储它的寄存器。Lua会为每一个活动函数分配一个栈,用于存放函数中的活动记录。每个函数的栈最多可以存放250个寄存器,因为栈的长度是用8位来表示的。有了这么多的寄存器,Lua的预编译器可以把所有的局部变量都存放在里面。这使得Lua在获取局部变量时非常高效。例如:假设a和b是局部变量,a=a+b的预编译会产生一条指令:;a是寄存器0b是寄存器1ADD001但是如果a和b都没有被声明为局部变量,那么预编译将生成以下命令:GETGLOBAL00;获取GETGLOBAL11;getbADD001;doaddSETGLOBAL00;seta所以你明白了:在写Lua代码的时候,应该尽量使用局部变量。下面是几个对比测试,大家可以将代码复制到自己的编辑器中进行测试。a=os.clock()fori=1,10000000dolocalx=math.sin(i)endb=os.clock()print(b-a)--1.113454将math.sin赋给局部变量sin:a=os.clock()localsin=math.sinfori=1,10000000dolocalx=sin(i)endb=os.clock()print(b-a)--0.75951直接使用math.sin,耗时1.11秒;使用局部变量sin来保存数学。sin,需要0.76秒。您可以获得30%的效率提升!关于表(tables)表在Lua中的使用非常频繁,因为表几乎替代了Lua中的所有容器。所以快速理解Lua底层是如何实现表的,对我们写Lua代码有好处。Lua的表分为两部分:数组(array)部分和散列(hash)部分。数组部分包含从1到n的所有整数键,所有其他键都存储在哈希部分。哈希部分实际上是一个哈希表。哈希表本质上是一个数组。它使用哈希算法将键转换为数组下标。如果下标冲突(即同一个下标对应两个不同的key),则在冲突的下标上创建一个链表,并在这个链表上串不同的key。这种解决冲突的方法称为:链地址法。当我们为表分配一个新的键值时,如果数组和哈希表已经满了,就会触发一次rehash。重新哈希是昂贵的。首先在内存中分配一个新长度的数组,然后对所有记录重新进行hash,将原来的记录转移到新数组中。新哈希表的长度是最接近所有元素个数的2的幂。创建空表时,数组的长度和散列部分都会被初始化为0,即不会为它们初始化数组。让我们看看当我们执行下面的代码时在Lua中会发生什么:locala={}fori=1,3doa[i]=trueend最初,Lua创建一个空表a,在第一次迭代中,a[1]=true触发了一次rehash,lua将array部分的长度设置为2^0,也就是1,hash部分还是空的。第二次迭代,a[2]=true再次触发rehash,将数组部分的长度设置为2^1,即2。***一次迭代,又触发了一次rehash,数组部分的长度设置为2^2,也就是4。下面的代码:a={}a.x=1;a.y=2;a.z=3和前面的代码类似,只是触发了表的hash部分的rehash三次。只有三个元素的表会执行三次rehash;但是,一百万个元素的表只会执行20次rehash,因为2^20=1048576>1000000。但是,如果创建了很多长度非常小的表(比如坐标点:point={x=0,y=0}),这可能会造成巨大的影响。如果您要创建许多非常小的表,您可以预填充它们以避免重新散列。比如:{true,true,true},Lua知道这个表有3个元素,所以Lua直接创建了一个长度为3个元素的数组。同理,{x=1,y=2,z=3},Lua会在它的hash部分创建一个长度为4的数组。以下代码的执行时间为1.53秒:a=os.clock()fori=1,2000000dolocala={}a[1]=1;a[2]=2;a[3]=3endb=os.clock()print(b-a)--1.528293如果我们在创建表的时候填写表的大小,只需要0.75秒,效率翻倍!a=os.clock()fori=1,2000000dolocala={1,1,1}a[1]=1;a[2]=2;a[3]=3结束b=os.clock()打印(b-a)--0.746453因此,当需要创建很多小表时,应预先填充表的大小。关于字符串与其他主流脚本语言不同,Lua以两种不同的方式实现字符串类型。***,所有字符串在Lua中只保存一份。当一个新的字符串出现时,Lua检查是否有相同的副本,如果没有,则创建它,否则,指向副本。这可以使字符串比较和表索引相当快,因为??比较字符串只需要检查引用是否一致;但是也降低了创建字符串的效率,因为Lua需要重新查找比较。其次,所有字符串变量只保存字符串引用,而不是它们的缓冲区。这使得字符串赋值非常高效。例如,在Perl中,$x=$y会将$y的整个缓冲区复制到$x的缓冲区。当字符串很长时,这个操作的开销会非常大。在Lua中,同一个赋值只复制引用,效率很高。但是只保存引用会减慢字符串连接的速度。在Perl中,$s=$s之间的效率差距。'x'和$s。='x'很醒目。前者将复制整个$s并在其末尾添加'x';而后者会直接在$x的缓冲区末尾插入'x'。由于后者不需要复制,所以它的效率与$s的长度无关,因为它非常高效。在Lua中,不支持第二个更快的操作。以下代码将花费6.65秒:a=os.clock()locals=''fori=1,300000dos=s..'a'endb=os.clock()print(b-a)--6.649481我们可以使用tableto模拟缓冲区,下面的代码只用了0.72秒,效率提高了9倍多:a=os.clock()locals=''localt={}fori=1,300000dot[#t+1]='a'ends=table.concat(t,'')b=os.clock()print(b-a)--0.07178所以:在大字符串拼接中,应该避免...应用table来模拟buffer,以及然后concat得到最终的字符串。#p#3R原则3R原则(3R规则)是三个原则的缩写:reducing,reusingandrecycle。3R原则是循环经济和环境保护的原则,但同样适用于Lua。减少有很多方法可以避免创建新对象并节省内存。例如:如果你的程序中使用了太多的表,你可以考虑将其改为数据结构。举个栗子。假设你的程序中有一个多边形类型,你使用一个表来存储多边形的顶点:polyline={{x=1.1,y=2.9},{x=1.1,y=3.7},{x=4.6,y=5.2},...}上面的数据结构非常自然易懂。但是每个顶点都需要一个散列部分来存储。如果放在数组部分,会减少内存占用:polyline={{1.1,2.9},{1.1,3.7},{4.6,5.2},...}当有100万个顶点时,内存将使用的153.3MB减少到107.6MB,但代价是代码的可读性降低。最变态的方法是:polyline={x={1.1,1.1,4.6,...},y={2.9,3.7,5.2,...}}一百万个顶点,内存只会占用32MB,相当原来的1/5。您需要在性能和代码可读性之间做出权衡。在循环中,我们更需要关注实例的创建。fori=1,ndolocalt={1,2,3,'hi'}--执行逻辑,但t不会改变...end我们应该在循环外创建在循环中不会改变的东西:localt={1,2,3,'hi'}fori=1,ndo--执行逻辑,但t不变...endReusing如果无法避免创建新对象,我们需要考虑重用旧对象。考虑以下代码:localt={}fori=1970,2000dot[i]=os.time({year=i,month=6,day=14})end在每次循环迭代时,新表{year=i,month=6,day=14},但只有年份是变量。以下代码重用了该表:localt={}localaux={year=nil,month=6,day=14}fori=1970,2000doaux.year=i;t[i]=os.time(aux)end另一种复用方式是缓存之前计算的内容,避免后续重复计算。后面遇到同样的情况,可以直接查表拿出来。这种方法其实就是动态规划效率高的原因,其实质就是用空间换取时间。RecyclingLua有自己的垃圾回收器,所以我们一般不需要考虑垃圾回收。了解Lua的垃圾回收可以让我们在编程上有更多的自由度。Lua的垃圾收集器是一种增量机制。也就是说,回收是在许多小步骤(增量)中完成的。频繁的垃圾回收可能会降低程序的运行效率。我们可以通过Lua的collectgarbage函数来控制垃圾收集器。collectgarbage函数提供了多个功能:停止垃圾收集,重启垃圾收集,强制一个收集周期,强制一个垃圾收集步骤,获取Lua占用的内存,以及影响垃圾收集频率和速度的两个参数。对于批处理Lua程序,停止垃圾回收collectgarbage("stop")会提高效率,因为当批处理程序结束时,所有内存都会被释放。当谈到垃圾收集器的步伐时,实际上很难一概而论。更快的垃圾收集会消耗更多CPU,但会释放更多内存,这也会减少CPU分页时间。只有通过仔细的实验??,才能知道哪种方法更合适。结束语写代码的时候要按照高标准来写,尽量避免事后优化。如果真的有性能问题,我们需要工具来量化效率,找到瓶颈,并针对它们进行优化。当然,优化之后还需要再测量一下,看是否优化成功。在优化中,我们会面临很多选择:代码可读性和运行效率,CPU换内存,内存换CPU等等。需要根据实际情况进行不断的实验,找到最终的平衡点。***,有两个***武器:***,使用LuaJIT,LuaJIT可以让你在不修改代码的情况下获得平均5倍左右的加速。查看x86/x64下LuaJIT的性能提升比例。第二,用C/C++写瓶颈部分。由于Lua和C有着天然的密切关系,所以可以将Lua和C混合起来进行编程。但是C和Lua之间的通信会抵消一些C带来的优势。注意:两者是不兼容的,你用C重写的Lua代码越多,LuaJIT带来的优化就越少。免责声明本文基于Lua语言创始人RobertoIerusalimschy对LuaProgrammingGems中LuaPerformanceTips的翻译。这篇文章没有直译,删减了很多,也算是一个注记。感谢Roberto对Lua的辛勤工作和奉献!原文链接:http://wuzhiwei.net/lua_performance/
