和在全部其余编程语言中同样,在Lua中,咱们依然应当遵循下述两条有关程序优化的箴言:html
原则1:不要作优化。程序员
原则2:暂时不要作优化(对专家而言)。算法
这两条原则对于Lua编程来讲尤为有意义,Lua正是因其性能而在脚本语言中鹤立鸡群。编程
固然,咱们都知道性能是编程中要考量的一个重要因素,指数级时间复杂度的算法会被认为是棘手的问题,绝非偶然。若是计算结果来得太迟,它就是无用的结果。所以,每个优秀的程序员都应该时刻平衡在优化代码时所花费的资源和执行代码时所节省的资源。小程序
优秀的程序员对于代码优化要提出的第一个问题是:“这个程序须要被优化吗?”若是(仅当此时)答案是确定的,第二个问题则是:“在哪里优化?”数组
要回答这样两个问题,咱们须要制定一些标准。在进行有效的性能评定以前,不该该作任何优化工做。有经验的程序员和初学者以前的区别并不是在于前者善于指出一个程序的主要性能开销所在,而是前者知道本身不善于作这件事情。缓存
几年前,Noemi Rodriguez和我开发了一个用于Lua的CORBA ORB[2]原型,以后演变为OiL。做为第一个原型,咱们的实现的目标是简洁。为防止对额外的C函数库的依赖,这个原型在序列化整数时使用少许四则运算来分离各个字节(转换为以256为底),且不支持浮点值。因为CORBA视字符串为字符序列,咱们的ORB最初也将Lua字符串转换为一个字符序列(也就是一个Lua表),而且将其和其余序列等同视之。性能优化
当咱们完成这个原型以后,咱们把它的性能和一个使用C++实现的专业ORB进行对比。因为咱们的ORB是使用Lua实现的,预期上咱们能够容忍它的速度要慢一些,可是对比结果显示它慢得太多了,让咱们很是失望。一开始,咱们把责任归结于Lua自己;后来咱们怀疑问题出在那些须要序列化整数的操做上。咱们使用了一个很是简单的性能分析器(Profiler),与在《Lua程序设计》[3]第23章里描述的那个没什么太大差异。出乎咱们意料的是,整数序列化并无明显拖慢程序的速度,由于并无太多整数须要序列化;反而是序列化字符串须要对低性能负很大责任。实际上,每一条CORBA消息都包含若干个字符串,即便咱们没有显式地操做字符串亦是如此。并且序列化每一条字符串都是一个性能开销巨大的工做,由于它须要建立一个新表,并使用单独的字符填充;而后序列化整个序列,其中须要依次序列化每一个字符。一旦咱们将字符串序列化做为一种特殊状况(而不是经过通用的序列化流程)从新实现,整个程序的性能就获得了显著的提高。咱们只是添加了几行代码,程序的性能已经和C++实现的那个版本有得一拼了[4]。数据结构
所以,咱们老是应该在优化性能以前进行性能测试。经过测试,才能了解到要优化什么;在优化后再次测试,来确认咱们的优化工做确实带来了性能的提高。闭包
一旦你决定必须优化你的Lua代码,本文将可能有所帮助。本文描述了一些优化方式,主要是展现在Lua中怎么作会更慢,怎么作又会更快。在这里,我将不会讨论一些通用的优化技巧,例如优化算法等等——固然,你应该掌握和使用这些技巧,有不少其余地方能够了解这方面的内容。本文主要讨论一些专门针对Lua的优化技巧,与此同时,我还会持续地测试小程序的时间和空间性能。若是没有特别注明的话,全部的测试都在一台Pentium IV 2.9GHz、1GB内存、运行Ubuntu 7.十、Lua 5.1.1的机器上进行。我常常会给出实际的测量结果(例如7秒),可是这只在和其余测量数据进行对比时有意义。而当我说一个程序比另外一个快X%时,意味着前者比后者少消耗X%的时间(也就是说,比另外一个程序快100%的程序的运行不须要时间);当我说一个程序比另外一个慢X%时,则是说后者比前者快X%(意即,比另外一个程序慢50%的程序消耗的时间是前者的两倍)。
基本事实
在运行任何代码以前,Lua都会把源代码翻译(预编译)成一种内部的格式。这种格式是一个虚拟机指令序列,与真实的CPU所执行的机器码相似。以后,这个内部格式将会被由一个包含巨大的switch结构的while循环组成的C代码解释执行,switch中的每一个case对应一条指令。
可能你已经在别处了解到,从5.0版开始,Lua使用一种基于寄存器的虚拟机。这里所说的虚拟机“寄存器”与真正的CPU寄存器并不相同,由于后者难于移植,并且数量很是有限。Lua使用一个栈(经过一个数组和若干索引来实现)来提供寄存器。每一个活动的函数都有一个激活记录,也就是栈上的一个可供该函数存储寄存器的片断。所以,每一个函数都有本身的寄存器[1]。一个函数可使用最多250个寄存器,由于每一个指令只有8位用于引用一个寄存器。
因为寄存器数目众多,所以Lua预编译器能够把全部的局部变量都保存在寄存器里。这样带来的好处是,访问局部变量会很是快。例如,若是a和b是局部变量,语句
a = a + b
将只会生成一个指令:
ADD 0 0 1
(假设a和b在寄存器里分别对应0和1)。做为对比,若是a和b都是全局变量,那么这段代码将会变成:
GETGLOBAL 0 0 ; a GETGLOBAL 1 1 ; b ADD 0 0 1 SETGLOBAL 0 0 ; a
所以,能够很简单地得出在Lua编程时最重要的性能优化方式:使用局部变量!
若是你想压榨程序的性能,有不少地方均可以使用这个方法。例如,若是你要在一个很长的循环里调用一个函数,能够预先将这个函数赋值给一个局部变量。好比说以下代码:
for i = 1, 1000000 do local x = math.sin(i) end
比下面这段要慢30%:
local sin = math.sin for i = 1, 1000000 do local x = sin(i) end
访问外部局部变量(或者说,函数的上值)没有直接访问局部变量那么快,但依然比访问全局变量要快一些。例以下面的代码片断:
function foo (x) for i = 1, 1000000 do x = x + math.sin(i) end return x end print(foo(10))
能够优化为在foo外声明一次sin:
local sin = math.sin function foo (x) for i = 1, 1000000 do x = x + sin(i) end return x end print(foo(10))
第二段代码比前者要快30%。
尽管比起其余语言的编译器来讲,Lua的编译器很是高效,可是编译依然是重体力活。所以,应该尽量避免运行时的编译(例如使用loadstring函数),除非你真的须要有如此动态要求的代码,例如由用户输入的代码。只有不多的状况下才须要动态编译代码。
例如,下面的代码建立一个包含返回常数值1到100000的若干个函数的表:
local lim = 10000 local a = {} for i = 1, lim do a[i] = loadstring(string.format("return %d", i)) end print(a[10]()) --> 10
执行这段代码须要1.4秒。
表
通常状况下,你不须要知道Lua实现表的细节,就可使用它。实际上,Lua花了不少功夫来隐藏内部的实现细节。可是,实现细节揭示了表操做的性能开销状况。所以,要优化使用表的程序(这里特指Lua程序),了解一些表的实现细节是颇有好处的。
Lua的表的实现使用了一些很聪明的算法。每一个Lua表的内部包含两个部分:数组部分和哈希部分。数组部分以从1到一个特定的n之间的整数做为键来保存元素(咱们稍后即将讨论这个n是如何计算出来的)。全部其余元素(包括在上述范围以外的整数键)都被存放在哈希部分里。
正如其名,哈希部分使用哈希算法来保存和查找键。它使用被称为开放地址表的实现方式,意思是说全部的元素都保存在哈希数组中。用一个哈希函数来获取一个键对应的索引;若是存在冲突的话(意即,若是两个键产生了同一个哈希值),这些键将会被放入一个链表,其中每一个元素对应一个数组项。当Lua须要向表中添加一个新的键,但哈希数组已满时,Lua将会从新哈希。从新哈希的第一步是决定新的数组部分和哈希部分的大小。所以,Lua遍历全部的元素,计数并对其进行归类,而后为数组部分选择一个大小,这个大小至关于能使数组部分超过一半的空间都被填满的2的最大的幂;而后为哈希部分选择一个大小,至关于正好能容纳哈希部分全部元素的2的最小的幂。
当Lua建立空表时,两个部分的大小都是0。所以,没有为其分配数组。让咱们看一看当执行下面的代码时会发生什么:
local a = {} for i = 1, 3 do a[i] = true end
这段代码始于建立一个空表。在循环的第一次迭代中,赋值语句
a[1] = true
触发了一次从新哈希;Lua将数组部分的大小设为1,哈希部分依然为空;第二次迭代时
a[2] = true
触发了另外一次从新哈希,将数组部分扩大为2.最终,第三次迭代又触发了一次从新哈希,将数组部分的大小扩大为4。
相似下面的代码
a = {} a.x = 1; a.y = 2; a.z = 3
作的事情相似,只不过增长的是哈希部分的大小。
对于大的表来讲,初期的几回从新哈希的开销被分摊到整个表的建立过程当中,一个包含三个元素的表须要三次从新哈希,而一个有一百万个元素的表也只须要二十次。可是当建立几千个小表的时候,从新哈希带来的性能影响就会很是显著。
旧版的Lua在建立空表时会预选分配大小(4,若是我没有记错的话),以防止在初始化小表时产生的这些开销。可是这样的实现方式会浪费内存。例如,若是你要建立数百万个点(表现为包含两个元素的表),每一个都使用了两倍于实际所需的内存,就会付出高昂的代价。这也是为何Lua再也不为新表预分配数组。
若是你使用C编程,能够经过Lua的API函数lua_createtable来避免从新哈希;除lua_State以外,它还接受两个参数:数组部分的初始大小和哈希部分的初始大小[1]。只要指定适当的值,就能够避免初始化时的从新哈希。须要警戒的是,Lua只会在从新哈希时收缩表的大小,所以若是在初始化时指定了过大的值,Lua可能永远不会纠正你浪费的内存空间。
当使用Lua编程时,你可能可使用构造式来避免初始化时的从新哈希。当你写下
{true, true, true}
时,Lua知道这个表的数组部分将会有三个元素,所以会建立相应大小的数组。相似的,若是你写下
{x = 1, y = 2, z = 3}
Lua也会为哈希部分建立一个大小为4的数组。例如,执行下面的代码须要2.0秒:
for i = 1, 1000000 do local a = {} a[1] = 1; a[2] = 2; a[3] = 3 end
若是在建立表时给定正确的大小,执行时间能够缩减到0.7秒:
for i = 1, 1000000 do local a = {true, true, true} a[1] = 1; a[2] = 2; a[3] = 3 end
可是,若是你写相似于
{[1] = true, [2] = true, [3] = true}
的代码,Lua还不够聪明,没法识别表达式(在本例中是数值字面量)指定的数组索引,所以它会为哈希部分建立一个大小为4的数组,浪费内存和CPU时间。
两个部分的大小只会在Lua从新哈希时从新计算,从新哈希则只会发生在表彻底填满后,Lua须要插入新的元素之时。所以,若是你遍历一个表并清除其全部项(也就是所有设为nil),表的大小不会缩小。可是此时,若是你须要插入新的元素,表的大小将会被调整。多数状况下这都不会成为问题,可是,不要期望能经过清除表项来回收内存:最好是直接把表自身清除掉。
一个能够强制从新哈希的猥琐方法是向表中插入足够多的nil。例如:
a = {} lim = 10000000 for i = 1, lim do a[i] = i end -- 建立一个巨表 print(collectgarbage("count")) --> 196626 for i = 1, lim do a[i] = nil end -- 清除全部元素 print(collectgarbage("count")) --> 196626 for i = lim + 1, 2 * lim do a[i] = nil end -- 建立一堆nil元素 print(collectgarbage("count")) --> 17
除非是在特殊状况下,我不推荐使用这个伎俩:它很慢,而且没有简单的方法能知道要插入多少nil才够。
你可能会好奇Lua为何不会在清除表项时收缩表。首先是为了不测试写入表中的内容。若是在赋值时检查值是否为nil,将会拖慢全部的赋值操做。第二,也是最重要的,容许在遍历表时将表项赋值为nil。例以下面的循环:
for k, v in pairs(t) do if some_property(v) then t[k] = nil – 清除元素 end end
若是Lua在每次nil赋值后从新哈希这张表,循环就会被破坏。
若是你想要清除一个表中的全部元素,只须要简单地遍历它:
for k in pairs(t) do t[k] = nil end
一个“聪明”的替代解决方案:
while true do local k = next(t) if not k then break end t[k] = nil end
可是,对于大表来讲,这个循环将会很是慢。调用函数next时,若是没有给定前一个键,将会返回表的第一个元素(以某种随机的顺序)。在此例中,next将会遍历这个表,从开始寻找一个非nil元素。因为循环老是将找到的第一个元素置为nil,所以next函数将会花费愈来愈长的时间来寻找第一个非nil元素。这样的结果是,这个“聪明”的循环须要20秒来清除一个有100,000个元素的表,而使用pairs实现的循环则只须要0.04秒。
经过使用闭包,咱们能够避免使用动态编译。下面的代码只须要十分之一的时间完成相同的工做:
function fk (k) return function () return k end end local lim = 100000 local a = {} for i = 1, lim do a[i] = fk(i) end print(a[10]()) --> 10