编写高效Lua代码的方法 - 2 - 表相关

来源:互联网 发布:linux ctrl c 编辑:程序博客网 时间:2024/06/03 15:40

翻译自《Lua Programming Gems》Chapter 2:Lua Performance Tips:Basic fact By Roberto Ierusalimschy

编写高效Lua代码的方法

表相关

通常情况下,你在使用表(table)的时候并不需要任何有关Lua如何实现表的细节。事实上,Lua竭尽全力地避免实现细节暴露给用户。但是这些细节还是在table操作的性能中暴露出来了。所以,为了高效地使用表,知道一点Lua如何实现table是有好处的。

    Lua使用了一些巧妙的算法来实现table。每个表包含两部分:数组(array)部分和哈希(hash)部分,数组部分保存整数键值(key)为1到n范围内的值(entry),n是一些独特的数值。(我们后面会讨论在某一刻n是怎么被计算出来的。)其他的值(包括整数下标超出1到n范围的)保存在哈希部分。

    顾名思义,哈希部分使用哈希算法来保存和查找里面的键值。它使用的是开发地址列表,所有的值都存在哈希数组里。哈希函数算出一个键值的主索引;如果发生碰撞(两个键值的哈希值是相同的),这些有相同主索引的键值将会被连成一个链表,每个元素在数组中占一个位置。

    当Lua需要在表中插入一个新的键值而此时哈希数组没有空位置时,Lua将会做一次重新哈希(rehash)。重新哈希的第一步是决定新的数组部分和哈希部分的大小。所以Lua会遍历表中的所有值,对这些值进行计数和分类,然后选择一个小于数组部分大小的2的最大指数值,使数组的一半以上会被使用。哈希部分的大小就是大于剩下的值(不能放在数组部分的值)的数量的最小2的指数值。

    当Lua创建一个空表的时候,其中的两部分的大小都是0,并且此时并没有赋予他们内存空间。下面我们来看看下面代码会发生什么事情:

local a = {}for i = 1, 3 do    a[i] =trueend

代码一开始创建一个空表。第一次循环的时候,赋值语句a[1]=true触发了一次重新哈希计算;Lua将表中的数组部分大小设为1,哈希部分还是空的。第二次循环的时候,赋值语句a[2]=true又触发了一次重新哈希计算,现在,表中的数组部分大小为2。最后,第三次循环又触发了一次重新哈希计算,数组部分的大小增大到4。

代码:

a = {}a.x = 1; a.y = 2; a.z = 3

做的事情是类似的,不过大小增长的是table中的哈希部分。

    对于大型的表,这些初始化开销将会被整个创建过程平摊:一个包含三个元素的表需要进行3次重新哈希计算,而一个包含了一百万个元素的表只需要20次。但是当你创建几千个小的表时,总开销就会很显著。

    老版本的Lua在创建空表的时候会为其预分配一些空位(如果没记错,是4),来避免创建较小的表时的开销。但是这样可能会出现浪费内存的情况。比如,如果你创建几百万个点(在表里面只存放了两个数字),那么每个点使用的内存将会是其真正需要的内存的两倍,你将会付出高昂的代价。这就是为什么现在的Lua没有为空表预分配空位。

    如果你是用C语言编程,你可以通过调用Lua的API函数lua_createtable来避免这些重新哈希计算。它在随处可见的lua_State中获取两个参数:新表数组部分的初始大小和哈希部分的初始大小。通过提供一个适当的初始大小给新表,可以很容易地避免这些初始化时的重新哈希计算。需要注意的是,Lua只有在进行重新哈希计算的时候,才会缩小表的大小。所以,如果你提供的初始大小比实际使用的大的话,Lua不会纠正你对空间的浪费。

    当你在Lua下面编程的时候,你可以通过构造来避免那些初始化的重新哈希计算。当你写下{true,true, true}的时候,Lua就会事先知道新的表的数组部分将会用到3个空位,并创建一个相应大小的表。类似的,当你写下{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] = 3end
如果以一个适当的初始大小来创建一个表的话,运行时间将会降低到0.7秒:
for i = 1, 1000000 do    local a ={true, true, true}    a[1] = 1;a[2] = 2; a[3] = 3end

但是,当你写下类似{[1] = true, [2] = true, [3] =true}的时候,Lua并没有聪明到检测到以上的表达式(这里指字面数字123)是在描述数组下标,所以它创建了一个哈希部分有四个空位的表,这浪费了内存和CPU时间。

    表的两个组成部分的大小只在表进行重新哈希计算的时候计算出来,而重新哈希计算只会在表已经被放满时需要插入一个新元素的时候发生。因此,当你遍历一个表并把其中的元素都删除的时候(就是把表里的值设为nil),表并不会缩小。当你插入一些新元素时,表才会重新改变其大小。通常这并不是一个问题:当你持续地删除和插入元素时(很多程序的典型情况),表的大小将保持稳定。你不应该通过在一个巨大的表中删除一些数据来节省空间:删除这个巨大的表会更好。

    有一种比较肮脏的手段来强迫表进行重新哈希计算,就是通过在表中插入足够的nil元素。看看以下例子:

a = {}lim = 10000000for i = 1, lim do a[i] = i end -- 创建一个巨大的表print(collectgarbage("count")) -->196626for i = 1, lim do a[i] = nil end -- 删除其所有的元素print(collectgarbage("count")) -->196626for 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    ifsome_property(v) then        t[k]= nil -- 删除这个元素    endend

如果Lua在表元素被赋nil值之后进行重新哈希,将会摧毁这个遍历。

    如果你想删除表中的所有元素,下面一个简单的循环式其中一种正确的方法:

for k in pairs(t) do    t[k] = nilend
另外一个“智能”的选择是下面的这个循环:
while true do    local k =next(t)    if not kthen break end    t[k] = nilend
     然而,这个循环再表很大的时候会很慢。当调用函数next时,如果没有传入前一个键值作为参数,函数next会返回表中的“第一个”元素(以一种随机的排序方法)。为了做到这个,next函数会从表的数组部分的开头开始遍历,查找非nil元素。随着循环将一个个的第一个元素设为nil,查找第一个非nil元素变得越来越久。最后,这个“智能”循环用了20秒时间来清除一个有100000元素的表;使用pairs的遍历表的循环则耗费了0.04秒。 
原创粉丝点击