编写高效Lua代码的方法 - 4 - 减少,重用,回收

来源:互联网 发布:养成类游戏源码 编辑:程序博客网 时间:2024/05/29 04:47

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

编写高效Lua代码的方法

减少,重用,回收(Reduce, Reuse, Recycle)

当处理Lua资源时,我们应当遵守跟利用地球资源一样的3R's原则。

    减少是最简单的一种途径。有几种方法可以避免创建对象。例如,如果你的程序使用了大量的表,你可以考虑改变它的数据表示方式。举个简单的例子,假如你的程序需要处理多边形。在Lua里表示一个多边形最自然的方式就是表示成一个点的列表,像这样:

polyline = { { x = 10.3, y = 98.5 },                 { x = 10.3, y = 18.3 },                 { x = 15.0, y = 98.5 },                 ...                }

尽管这是很自然的一种方式,但是当多边形很大的时候,由于每个点都要用一个表来存储,这种方式就显得有点不太经济了。第一种改进方法是该用数组来存储,这会内存使用量减少:

polyline = { { 10.3, 98.5 },                 { 10.3, 18.3 },                 { 15.0, 98.5 },                 ...                }

一个有一百万个点的多边形,这种改变会将内存使用从95KB降到65KB。当然,你牺牲了程序的可读性作为代价:p[i].x要比p[i][1]让人容易看得懂。

    另一个更经济的方法是用一个列表来存储x轴的值,另一个列表存储y轴的值:

polyline = { x = { 10.3, 10.3, 15.0, ...},                  y = { 98.5, 18.3, 98.5, ...}                }

之前的p[i].x就是现在的p.x[i]。使用这种方式,一个有一百万个点的多边形使用的内存只有24KB。

    循环体内是找到降低不必要资源创建的地方。例如,如果在一个循环中创建了一个不会改变内容的表,你可以把表放在循环体之外,或者甚至放在执行这段代码的函数之外。比较:

function foo (...)    for i =1, n do       local t = {1, 2, 3, "hi"}        -- 做一些不改变t的操作。        ...    endend

local t = {1, 2, 3, "hi"} -- 一劳永逸function foo (...)    for i =1, n do        -- 做一些不改变t的操作。        ...    endend

同样的技巧还可以用到闭包中,只要不要将其移至闭包需要的变量的作用域之外。例如,考虑一下函数:

function changenumbers (limit, delta)    for linein io.lines() do        line= string.gsub(line, "%d+", function (num)            num =tonumber(num)            ifnum >= limit then return tostring(num + delta) end            --else return nothing, keeping the original number        end)       io.write(line, "\n")    endend

我们可以通过将内嵌的函数放在循环体之外来避免读取每行数据都要创建一个闭包:

function changenumbers (limit, delta)    localfunction aux (num)        num =tonumber(num)        if num>= limit then return tostring(num + delta) end        end    for linein io.lines() do        line =string.gsub(line, "%d+", aux)       io.write(line, "\n")    endend

然而,我们不能将函数aux搬到函数changenumbers之外,因为在那里函数aux不能访问变量limit和delta。

    对于大多数字符串操作,我们都可以通过下标在已存在的字符串上工作,从而减少字符串的创建。例如,函数string.find返回字符串上匹配正则表达式的下标,而不是返回一个匹配的字符串。返回下标,就避免了在成功匹配时创建一个新的(子)字符串。开发者需要时,可以通过函数string.sub来获取匹配的子字符串。 

    当我们不能避免使用新的对象的时候,我们还是可以通过重用来避免创建这些对象。考虑字符串的重用是没有必要的,因为Lua已经替我们做了这些工作:Lua总是将用到的字符串内部化,不会放过任何重用的机会。重用表则是比较有效。举一个常见的例子,让我们回到在一个循环体内部创建表的情况。不同的是这次的表的内容不是固定不变的。不过,我们往往还是可以简单地更改表的内容从而能够在所有迭代中重用这个表。考虑一下代码:

local t = {}for i = 1970, 2000 do    t[i] =os.time({year = i, month = 6, day = 14})end一下是与之等价的代码,但是重用了表:local t = {}local aux = {year = nil, month = 6, day = 14}for i = 1970, 2000 do    aux.year =i    t[i] =os.time(aux)end

另一种比较有用的实现重用的方法是记忆化(memoizing)。原理很简单:对于一个给定的某些输入值,保存其计算结果,当同样的值输入时,程序只需重用之前保存的结果。

    LPeg,Lua中一个新的模式匹配的包,有趣地使用了记忆化方法。LPeg把每个模式都编译成一种内部的格式,负责匹配的分析器把这些格式的编码看成一个个“程序”。这个编译动作相对于匹配动作来说是比较耗费资源的。所以,LPeg把编译的结果记忆化,从而达到重用的目的。它通过在一个表里面把模式字符串以及其对应的内部格式关联起来来实现。

    记忆化方法的一个比较普遍的问题是,保存之前计算结果所耗费的空间可能会掩盖重用这些结果的好处。为了在Lua中解决这个问题,我们可以使用一个弱表来保存计算结果,这样,不用的结果就会从表中被删除。

    在Lua里,有了更高等的函数(译注,Lua的函数是一等类型,即Lua处理函数和变量的方式是一样的),我们可以定义一个通用的记忆化函数:


function memoize (f)    local mem= {} -- memoizing table   setmetatable(mem, {__mode = "kv"}) -- make it weak    returnfunction (x) -- new version of ’f’, with memoizing        local r = mem[x]        if r== nil then -- no previous result?           r = f(x) -- calls original function           mem[x] = r -- store result for reuse        end       return r    endend
对于一个给定的函数f,memoize(f)返回一个新的函数,这个函数会返回跟f一样的结果,但是会吧结果记录下来。例如,我们可以重新定义loadstring函数的一个记忆化版本:
loadstring = memoize(loadstring)

我们像使用老 函数一样使用这个新函数,但是如果我们加载很多重复的字符串的时候,我们将会从性能上获得很大的收益。

    如果你的程序创建和释放过多的协程的时候,回收是一个提高程序性能的又一选择。目前协程的API并没有直接提供重用一个协程的方法,但是我们可以规避这个限制。考虑以下协程:

co = coroutine.create(function (f)    while fdo        f =coroutine.yield(f())    endend

这个协程接受一个作业(一个将要被执行的函数),运行这个作业,结束后等待下一个作业。

    Lua中的大多数回收都是由垃圾收集器自动完成的。Lua使用一个增量垃圾收集器。这意味着收集器每次都执行一小步动作(增量地),跟程序一起交错执行。每一步的工作量是跟程序的内存申请量成正比的:Lua申请了多少内存,垃圾收集器就做相当比例的工作。程序消耗内存越快,收集器就越快地尝试回收内存。

    如果我们在程序中遵守减少和重用的准则,通常收集器没有太多的事情做。但是有时候我们不能避免创建大量的垃圾,这时收集器的工作就变得繁重了。Lua的垃圾收集器是用来调整程序平衡的,所以再大多数程序中,它的表现都是很合理的。但是有些特殊情况,我们还是可以通过更好地调整收集器提高程序的性能的。

    在Lua里,我们可以通过调用函数collectgarbage来控制垃圾收集器,在C里则是调用lua_gc。尽管接口不同,以上两个函数基本上提供了相同的功能。接下来的讨论我会使用Lua的接口,但是这种操作往往在C里面做会更好。

    函数collectgarbage提供了几种功能:它可以停止和重启收集器,强制进行一次完成的收集,强制执行一步收集,得到Lua使用的总内存量,更改两个影响到收集步伐的参数。所有这些操作在需要大量内存的程序里都有其用武之地。

    一些批处理程序,它们创建了若干结构体,根据那些结构体产生一些输出,然后退出(比如编译器)。“永远”停止收集器将是一个好选择。对于那些程序,垃圾收集是比较浪费时间的,因为可回收的垃圾很少,并且程序一旦退出,所有的内存就会被释放了。

    对于一些非批处理的程序,永远关闭收集器就不是个好选择了。尽管如此,在程序的一些关键时间点关闭收集器还是有好处的。必要的时候,还可以由程序来完全控制垃圾收集器:收集器总是处于关闭状态,只有程序显式地要求执行一个步骤或者执行一个完整的回收时,收集器才开始工作。例如,有些事件驱动的平台会提供一个idle函数,这个函数会在没有事件可以处理时被调用。这是执行垃圾收集的最好时刻。(在Lua5.1中,每次在收集器关闭时强制执行一些收集工作都会使收集器自动启动。所以为了保持收集器做完马上关闭,你必须在强制执行一些收集操作之后马上调用collectgarbage("stop")。)

    最后一个方法,你可以尝试改变收集器的参数。收集器由两个参数控制其收集步伐。第一个是“pause”(暂停),控制收集器在一轮回收结束后隔多久才开始下一轮的回收。第二个参数是“stepmul”(即 step multiplier,步伐递增),控制收集器每一步要做多少工作。粗略地讲,暂停间隔越小,步伐递增越大,收集器工作就越快。

    这些参数对一个程序的总体性能的影响是很难预测的。很明显地,一个越快的收集器每秒耗费的CPU周期就越多;但是另一方面,这将会减少程序的总内存占用量,从而减少页面切换的几率。只有认真的实验能够让你给这些参数设定一个最好的值。

原创粉丝点击