编写高性能的Lua代码
最佳实践去写出高性能的代码,而不是编写了一堆垃圾代码后,再考虑优化。相信工作后大家都会对事后的优化的繁琐都深有体会。
一旦你决定编写高性能的Lua代码,下文将会指出在Lua中哪些代码是可以优化的,哪些代码会是运行缓慢的,然后怎么去优化它们。 使用local在代码运行前,Lua会把源码预编译成一种中间码,类似于Java的虚拟机。这种格式然后会通过C的解释器进行解释,整个过程其实就是通过一个 自Lua 5.0之后,Lua采用了一种类似于寄存器的虚拟机模式。Lua用栈来储存其寄存器。每一个活动的函数,Lua都会其分配一个栈,这个栈用来储存函数里的活动记录。每一个函数的栈都可以储存至多250个寄存器,因为栈的长度是用8个比特表示的。 有了这么多的寄存器,Lua的预编译器能把所有的local变量储存在其中。这就使得Lua在获取local变量时其效率十分的高。 举个栗子:假设a和b为local变量, ;a是寄存器0 b是寄存器1 ADD 0 0 1 但是若a和b都没有声明为local变量,则预编译会产生如下指令: GETGLOBAL 0 0 ;get a GETGLOBAL 1 1 ;get b ADD 0 0 1 ;do add SETGLOBAL 0 0 ;set a 所以你懂的:在写Lua代码时,你应该尽量使用local变量。 以下是几个对比测试,你可以复制代码到你的编辑器中,进行测试。 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 :
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%的效率提升!
关于表(table)表在Lua中使用十分频繁,因为表几乎代替了Lua的所有容器。所以快速了解一下Lua底层是如何实现表,对我们编写Lua代码是有好处的。 Lua的表分为两个部分:数组(array)部分和哈希(hash)部分。数组部分包含所有从1到n的整数键,其他的所有键都储存在哈希部分中。 哈希部分其实就是一个哈希表,哈希表本质是一个数组,它利用哈希算法将键转化为数组下标,若下标有冲突(即同一个下标对应了两个不同的键),则它会将冲突的下标上创建一个链表,将不同的键串在这个链表上,这种解决冲突的方法叫做:链地址法。 当我们把一个新键值赋给表时,若数组和哈希表已经满了,则会触发一个再哈希(rehash)。再哈希的代价是高昂的。首先会在内存中分配一个新的长度的数组,然后将所有记录再全部哈希一遍,将原来的记录转移到新数组中。新哈希表的长度是最接近于所有元素数目的2的乘方。 当创建一个空表时,数组和哈希部分的长度都将初始化为0,即不会为它们初始化任何数组。让我们来看下执行下面这段代码时在Lua中发生了什么: local a = {} for i=1,3 do a[i] = true end 最开始,Lua创建了一个空表a,在第一次迭代中, 下面这段代码: a = {} a.x = 1; a.y = 2; a.z = 3 与上一段代码类似,只是其触发了三次表中哈希部分的rehash而已。 只有三个元素的表,会执行三次rehash;然而有一百万个元素的表仅仅只会执行20次rehash而已,因为 如果你有很多非常多的很小的表需要创建时,你可以将其预先填充以避免rehash。比如: 以下代码执行时间为1.53秒: 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秒,一倍的效率提升! 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中, 但是只保存引用会降低在字符串连接时的速度。在Perl中, 由于后者不需要进行拷贝,所以其效率和$s的长度无关,因为十分高效。 在Lua中,并不支持第二种更快的操作。以下代码将花费6.65秒: 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倍多的效率提升: 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 所以:在大字符串连接中,我们应避免 3R原则3R原则(the rules of 3R)是:减量化(reducing),再利用(reusing)和再循环(recycling)三种原则的简称。 3R原则本是循环经济和环保的原则,但是其同样适用于Lua。 Reducing有许多办法能够避免创建新对象和节约内存。例如:如果你的程序中使用了太多的表,你可以考虑换一种数据结构来表示。 举个栗子。假设你的程序中有多边形这个类型,你用一个表来储存多边形的顶点: 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 },... } 一百万个顶点时,内存将会由153.3MB减少到107.6MB,但是代价是代码的可读性降低了。 最变态的方法是: 2 3 4 polyline = { x = {1.1,1.1,4.6,...},y = {2.9,3.7,5.2,...} } 一百万个顶点,内存将只占用32MB,相当于原来的1/5。你需要在性能和代码可读性之间做出取舍。 在循环中,我们更需要注意实例的创建。 for i=1,n do local t = {1,2,3,'hi'} --执行逻辑,但t不更改 ... end 我们应该把在循环中不变的东西放到循环外来创建: local t = {1,'hi'} for i=1,n do --执行逻辑,但t不更改 ... end Reusing如果无法避免创建新对象,我们需要考虑重用旧对象。 考虑下面这段代码: local t = {} for i = 1970,2000 do t[i] = os.time({year = i,month = 6,day = 14}) end 在每次循环迭代中,都会创建一个新表 下面这段代码重用了表: local t = {} local aux = {year = nil,day = 14} for i = 1970,2000 do aux.year = i; t[i] = os.time(aux) end另一种方式的重用,则是在于缓存之前计算的内容,以避免后续的重复计算。后续遇到相同的情况时,则可以直接查表取出。这种方式实际就是 动态规划效率高的原因所在,其本质是用空间换时间。 RecyclingLua自带垃圾回收器,所以我们一般不需要考虑垃圾回收的问题。 了解Lua的垃圾回收能使得我们编程的自由度更大。 Lua的垃圾回收器是一个增量运行的机制。即回收分成许多小步骤(增量的)来进行。 频繁的垃圾回收可能会降低程序的运行效率。 我们可以通过Lua的
对于批处理的Lua程序来说,停止垃圾回收 对于垃圾回收器的步幅来说,实际上很难一概而论。更快幅度的垃圾回收会消耗更多CPU,但会释放更多内存,从而也降低了CPU的分页时间。只有小心的试验,我们才知道哪种方式更适合。 结语我们应该在写代码时,按照高标准去写,尽量避免在事后进行优化。 如果真的有性能问题,我们需要用工具量化效率,找到瓶颈,然后针对其优化。当然优化过后需要再次测量,查看是否优化成功。 在优化中,我们会面临很多选择:代码可读性和运行效率,CPU换内存,内存换CPU等等。需要根据实际情况进行不断试验,来找到最终的平衡点。 最后,有两个终极武器: 第一、使用LuaJIT,LuaJIT可以使你在不修改代码的情况下获得平均约5倍的加速。查看LuaJIT在x86/x64下的性能提升比。 第二、将瓶颈部分用C/C++来写。因为Lua和C的天生近亲关系,使得Lua和C可以混合编程。但是C和Lua之间的通讯会抵消掉一部分C带来的优势。 注意:这两者并不是兼容的,你用C改写的Lua代码越多,LuaJIT所带来的优化幅度就越小。 声明这篇文章是基于Lua语言的创造者Roberto Ierusalimschy在Lua Programming Gems 中的Lua Performance Tips翻译改写而来。本文没有直译,做了许多删节,可以视为一份笔记。 感谢Roberto在Lua上的辛勤劳动和付出! 原文地址:http://wuzhiwei.net/lua_performance/ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |