Lua性能优化技巧(五):削减、重用和回收

当处理Lua资源时,我们也应该遵循提倡用于地球资源的3R原则——Reduce, Reuse and Recycle,即削减、重用和回收。

削减是最简单的方式。有很多方法可以避免使用新的对象,例如,如果你的程序使用了太多的表,可以考虑改变数据的表述形式。一个最简单的例子,假设你的程序需要操作折线,最自然的表述形式是:

代码如下:

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表的事情
          --...
     end
end

代码如下:

local t = {1, 2, 3, "hi"} -- 创建t,一劳永逸
function foo (...)
    for i = 1, n do
        --做一些不会改变t表的事情
        --...
    end
end

相同的技巧亦可用于闭包,只要你不把它们移到需要它们的作用域之外。例如下面的函数:

代码如下:

function changenumbers (limit, delta)
    for line in io.lines() do
        line = string.gsub(line, "%d+", function (num)
            num = tonumber(num)
            if num >= limit then return tostring(num + delta) end
            -- 否则不返回任何值,保持原有数值
        end)
        io.write(line, "\n")
    end
end

我们可以通过将内部的函数移到循环外面来避免为每次迭代创建新的闭包:

代码如下:

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

但是,我们不能把aux移到changenumbers函数之外,因为aux需要访问limit和delta。

对于多种字符串处理,我们可以通过使用现有字符串的索引来减少对创建新字符串的需要。例如,string.find函数返回它找到指定模式的位置索引,而不是匹配到的字符串。通过返回索引,它避免了在成功匹配时创建新的字符串。当有必要时,程序员可以通过调用string.sub来获取匹配的子串[1]。

当我们无法避免使用新的对象时,我们依然可以通过重用来避免创建新的对象。对于字符串来说,重用没什么必要,因为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

实现重用的一个尤其有效的方式是缓存化[2]。基本思想非常简单,将指定输入对应的计算结果存储下来,当下一次再次接受相同的输入时,程序只需简单地重用上次的计算结果。

LPeg,Lua的一个新的模式匹配库,就使用了一个有趣的缓存化处理。LPeg将每个模式字符串编译为一个内部的用于匹配字符串的小程序,比起匹配本身而言,这个编译过程开销很大,因此LPeg将编译结果缓存化以便重用。只需一个简单的表,以模式字符串为键、编译后的小程序为值进行记录。

使用缓存化时常见的一个问题是,存储计算结果所带来的内存开销大过重用带来的性能提升。为了解决这个问题,我们可以在Lua里使用一个弱表来记录计算结果,因此没有使用到的结果最终将会被回收。

在Lua中,利用高阶函数,我们可以定义一个通用的缓存化函数:

代码如下:

function memoize (f)
    local mem = {} -- 缓存化表
    setmetatable(mem, {__mode = "kv"}) -- 设为弱表
    return function (x) -- ‘f'缓存化后的新版本
        local r = mem[x]
        if r == nil then --没有之前记录的结果?
            r = f(x) --调用原函数
            mem[x] = r --储存结果以备重用
        end
        return r
    end
end

对于任何函数f,memoize(f)返回与f相同的返回值,但是会将之缓存化。例如,我们可以重新定义loadstring为一个缓存化的版本:

loadstring = memoize(loadstring)
新函数的使用方式与老的完全相同,但是如果在加载时有很多重复的字符串,性能会得到大幅提升。

如果你的程序创建和删除太多的协程,循环利用将可能提高它的性能。现有的协程API没有直接提供重用协程的支持,但是我们可以设法绕过这一限制。对于如下协程:

代码如下:

co = coroutine.create(function (f)
    while f do
        f = coroutine.yield(f())
    end
end)

这个协程接受一项工作(运行一个函数),执行之,并且在完成时等待下一项工作。

Lua中的多数回收都是通过垃圾回收器自动完成的。Lua使用渐进式垃圾回收器,意味着垃圾回收工作会被分成很多小步,(渐进地)在程序的允许过程中执行。渐进的节奏与内存分配的速度成比例,每当分配一定量的内存,就会按比例地回收相应的内存;程序消耗内存越快,垃圾回收器尝试回收内存也就越快。

如果我们在编写程序时遵循削减和重用的原则,通常垃圾回收器不会有太多的事情要做。但是有时我们无法避免制造大量的垃圾,垃圾回收器的工作也会变得非常繁重。Lua中的垃圾回收器被调节为适合平均水平的程序,因此它在多数程序中工作良好。但是,在特定的时候我们可以通过调整垃圾回收器来获取更好的性能。通过在Lua中调用函数collectgarbage,或者在C中调用lua_gc,来控制垃圾回收器。它们的功能相同,只不过有不同的接口。在本例中我将使用Lua接口,但是这种操作通常在C中进行更好。

collectgarbage函数提供若干种功能:它可以停止或者启动垃圾回收器、强制进行一次完整的垃圾回收、获取Lua占用的总内存,或者修改影响垃圾回收器工作节奏的两个参数。它们在调整高内存消耗的程序时各有用途。

“永远”停止垃圾回收器可能对于某些批处理程序很有用。这些程序创建若干数据结构,根据它们生产出一些输出值,然后退出(例如编译器)。对于这样的程序,试图回收垃圾将会是浪费时间,因为垃圾量很少,而且内存会在程序执行完毕后完整释放。

对于非批处理程序,停止垃圾回收器则不是个好主意。但是,这些程序可以在某些对时间极度敏感的时期暂停垃圾回收器,以提高时间性能。如果有需要的话,这些程序可以获取垃圾回收器的完全控制,使其始终处于停止状态,仅在特定的时候显式地进行一次强制的步进或者完整的垃圾回收。例如,很多事件驱动的平台都提供一个选项,可以设置空闲函数,在没有消息需要处理时调用。这正是调用垃圾回收的绝好时机(在Lua 5.1中,每当你在垃圾回收器停止的状态下进行强制回收,它都会恢复运转,因此,如果要保持垃圾回收器处于停止状态,必须在强制回收后立刻调用collectgarbage("stop"))。

最后,你可能希望实施调整回收器的参数。垃圾回收器有两个参数用于控制它的节奏:第一个,称为暂停时间,控制回收器在完成一次回收之后和开始下次回收之前要等待多久;第二个参数,称为步进系数,控制回收器每个步进回收多少内容。粗略地来说,暂停时间越小、步进系数越大,垃圾回收越快。这些参数对于程序的总体性能的影响难以预测,更快的垃圾回收器显然会浪费更多的CPU周期,但是它会降低程序的内存消耗总量,并可能因此减少分页。只有谨慎地测试才能给你最佳的参数值。

[1] 如果标准库提供一个用于对比两个子串的函数可能会是一个好主意,这样我们无需将子串解出(会创建新的字符串)即可检查字符串中的特定值。

[2] 缓存化,原文memoize

(0)

相关推荐

  • Lua性能优化技巧(一):前言

    和在所有其他编程语言中一样,在Lua中,我们依然应当遵循下述两条有关程序优化的箴言: 原则1:不要做优化. 原则2:暂时不要做优化(对专家而言). 这两条原则对于Lua编程来说尤其有意义,Lua正是因其性能而在脚本语言中鹤立鸡群. 当然,我们都知道性能是编程中要考量的一个重要因素,指数级时间复杂度的算法会被认为是棘手的问题,绝非偶然.如果计算结果来得太迟,它就是无用的结果.因此,每一个优秀的程序员都应该时刻平衡在优化代码时所花费的资源和执行代码时所节省的资源. 优秀的程序员对于代码优化要提出的第

  • Lua性能优化技巧(三):关于表

    一般情况下,你不需要知道Lua实现表的细节,就可以使用它.实际上,Lua花了很多功夫来隐藏内部的实现细节.但是,实现细节揭示了表操作的性能开销情况.因此,要优化使用表的程序(这里特指Lua程序),了解一些表的实现细节是很有好处的. Lua的表的实现使用了一些很聪明的算法.每个Lua表的内部包含两个部分:数组部分和哈希部分.数组部分以从1到一个特定的n之间的整数作为键来保存元素(我们稍后即将讨论这个n是如何计算出来的).所有其他元素(包括在上述范围之外的整数键)都被存放在哈希部分里. 正如其名,哈

  • Lua性能优化技巧(六):最后的提示

    正如我们在前言里所说,优化是一个技巧性很强的工作,从程序是否需要优化开始,有若干个方面的内容需要考量.如果程序真的有性能问题,那么我们应该将精力集中于优化哪里和如何优化. 我们在这里讨论的技巧既不是唯一的,也不是最重要的方面.我们在这里专注于讨论专门针对Lua的优化方式,因为有很多其他的方式可以了解通用的程序优化技巧. 在本文结束之前,我还想介绍两种从更大的尺度上优化Lua程序性能的方式,但是它们都牵涉到Lua代码之外的修改.第一个是使用LuaJIT[1],一个Lua的即时编译器,由Mike P

  • Lua性能优化技巧(二):基本事实

    在运行任何代码之前,Lua都会把源代码翻译(预编译)成一种内部的格式.这种格式是一个虚拟机指令序列,与真实的CPU所执行的机器码类似.之后,这个内部格式将会被由一个包含巨大的switch结构的while循环组成的C代码解释执行,switch中的每个case对应一条指令. 可能你已经在别处了解到,从5.0版开始,Lua使用一种基于寄存器的虚拟机.这里所说的虚拟机"寄存器"与真正的CPU寄存器并不相同,因为后者难于移植,而且数量非常有限.Lua使用一个栈(通过一个数组和若干索引来实现)来提

  • Lua性能优化技巧(四):关于字符串

    与表类似,了解Lua如何实现字符串可以让你更高效地使用它. Lua实现字符串的方式与多数其他脚本语言所采用的两种主要方式都不相同.首先,Lua中的所有字符串都是内部化[1]的,这意味着Lua维护着任何字符串的一个单一拷贝.当一个新字符串出现时,Lua检查是否有现成的拷贝,如果有的话,重用之.内部化使得诸如字符串对比和索引表之类的操作非常快速,但是会降低创建字符串的速度. 第二,Lua中的变量从不存储字符串,只是引用它们.这种实现方式可以加快很多字符串操作,例如在Perl中,当你写类似于$x=$y

  • Lua性能优化技巧(五):削减、重用和回收

    当处理Lua资源时,我们也应该遵循提倡用于地球资源的3R原则--Reduce, Reuse and Recycle,即削减.重用和回收. 削减是最简单的方式.有很多方法可以避免使用新的对象,例如,如果你的程序使用了太多的表,可以考虑改变数据的表述形式.一个最简单的例子,假设你的程序需要操作折线,最自然的表述形式是: 复制代码 代码如下: polyline = {     { x = 10.3, y = 98.5 },     { x = 10.3, y = 18.3 },     { x = 1

  • jQuery性能优化技巧分析

    本文较为详细分析了jQuery性能优化技巧.分享给大家供大家参考.具体分析如下: 一.使用最新版本的jQuery类库 jQuery新版本会较上个版本进行Bug修复和一些优化,不过需要注意的是,在更换版本之后,不要忘记测试你的代码,毕竟有时候不是完全向后兼容的. 二.使用合适的选择器 jQuery选择器性能最佳到最差方式如下: id选择器,如$('#id', context) 标签选择器,如$('p', context) class选择器,如$('.class', context) 属性选择器,如

  • Python 代码性能优化技巧分享

    如何进行 Python 性能优化,是本文探讨的主要问题.本文会涉及常见的代码优化方法,性能优化工具的使用以及如何诊断代码的性能瓶颈等内容,希望可以给 Python 开发人员一定的参考. Python 代码优化常见技巧 代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构.优化.扩展以及文档相关的事情通常需要消耗 80% 的工作量.优化通常包含两方面的内容:减小代码的体积,提高代码的运行效率. 改进算法,选择合适的数据结构 一个

  • Java性能优化技巧汇总

    本文实例汇总了Java性能优化技巧.分享给大家供大家参考.具体分析如下: 这里参考了些书籍,网络资源整理出来,适合于大多数Java应用 在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身.养成良好的编码习惯非常重要,能够显著地提升程序性能. 1.尽量使用final修饰符. 带有final修饰符的类是不可派生的.在JAVA核心API中,有许多应用final的例子,例如java.lang.String.为String类指定final防止了使用者覆盖length()方法.另外,如

  • Vue.js九个性能优化技巧(值得收藏)

    目录 Functional components Child component splitting Local variables Reuse DOM with v-show KeepAlive Deferred features Time slicing Non-reactive data Virtual scrolling 总结 参考资料 这篇文章主要参考了 Vue.js 核心成员Guillaume Chau在 19 年美国的 Vue conf 分享的主题:9 Performance se

随机推荐