Lua性能优化技巧

时间:2021-02-13 06:06:19
前言

和在所有其他编程语言中一样,在Lua中,我们依然应当遵循下述两条有关程序优化的箴言:

原则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.10、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

访问外部局部变量(或者说,函数的上值)没有直接访问局部变量那么快,但依然比访问全局变量要快一些。例如下面的代码片段:

Lua性能优化技巧
function foo (x)
    for i = 1, 1000000 do
        x = x + math.sin(i)
    end
    return x
end

print(foo(10))
Lua性能优化技巧

可以优化为在foo外声明一次sin:

Lua性能优化技巧
local sin = math.sin
function foo (x)
    for i = 1, 1000000 do
        x = x + sin(i)
    end
    return x
end

print(foo(10))
Lua性能优化技巧

第二段代码比前者要快30%。

尽管比起其他语言的编译器来说,Lua的编译器非常高效,但是编译依然是重体力活。因此,应该尽可能避免运行时的编译(例如使用loadstring函数),除非你真的需要有如此动态要求的代码,例如由用户输入的代码。只有很少的情况下才需要动态编译代码。

例如,下面的代码创建一个包含返回常数值1到100000的若干个函数的表:

Lua性能优化技巧
local lim = 10000
local a = {}
for i = 1, lim do
    a[i] = loadstring(string.format("return %d", i))
end

print(a[10]()) --> 10
Lua性能优化技巧

执行这段代码需要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。例如:

Lua性能优化技巧
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
Lua性能优化技巧

除非是在特殊情况下,我不推荐使用这个伎俩:它很慢,并且没有简单的方法能知道要插入多少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秒。

通过使用闭包,我们可以避免使用动态编译。下面的代码只需要十分之一的时间完成相同的工作:

Lua性能优化技巧
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
Lua性能优化技巧

字符串

与表类似,了解Lua如何实现字符串可以让你更高效地使用它。

Lua实现字符串的方式与多数其他脚本语言所采用的两种主要方式都不相同。首先,Lua中的所有字符串都是内部化[1]的,这意味着Lua维护着任何字符串的一个单一拷贝。当一个新字符串出现时,Lua检查是否有现成的拷贝,如果有的话,重用之。内部化使得诸如字符串对比和索引表之类的操作非常快速,但是会降低创建字符串的速度。

第二,Lua中的变量从不存储字符串,只是引用它们。这种实现方式可以加快很多字符串操作,例如在Perl中,当你写类似于$x=$y的代码、$y是一个字符串时,赋值操作会将字符串的内容从$y的缓冲区复制到$x的缓冲区。如果这个字符串很长,这个操作的开销就很大。而在Lua中,这个赋值仅仅是一次指针的复制。

然而,这种引用实现会降低特定方式的字符串连接的速度。在Perl中,操作$s = $s . "x"和$s .= "x"区别非常大,对于前者,你获得了$s的一个拷贝,并且追加"x"到它的尾部;而对于后者,"x"只是简单地被追加到$s所维护的内部缓冲区的尾部。因此,后者无关于字符串的长度(假设缓冲区足够放下追加的文本)。如果把这两句代码放进循环里,它们的区别就是线性和二次算法的区别。例如,下述循环需要大约五分钟来读取一个5MB的文件:

$x = "";
while (<>)
{
    $x = $x . $_;
}

如果我们把

$x = $x . $_

改为

$x .= $_

耗时将会降低为0.1秒!

Lua没有提供第二种,也就是更快速的方式,因为它的变量没有内部缓冲区。因此,我们需要一个显式的缓冲区:一个包含字符串片段的表来完成这项工作。下面的循环读取相同的5MB的文件,需要0.28秒,虽然没有Perl那么快,也还算不错:

local t = {}
for line in io.lines() do
    t[#t + 1] = line
end
s = table.concat(t, "\n")
 
 

资源回收

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

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

Lua性能优化技巧
polyline =
{
    { x = 10.3, y = 98.5 },
    { x = 10.3, y = 18.3 },
    { x = 15.0, y = 98.5 },
    --...
}
Lua性能优化技巧

尽管很自然,这种表述形式对于大规模的折线来说却不够经济,因为它的每个点都需要用一个表来描述。第一种替代方式是使用数组来记录,可以省点内存:

Lua性能优化技巧
polyline =
{
     { 10.3, 98.5 },
     { 10.3, 18.3 },
     { 15.0, 98.5 },
     --...
}
Lua性能优化技巧

对于一个有一百万个点的折线来说,这个修改可以把内存占用从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。

循环是寻找降低垃圾回收次数的机会的好地方。例如,如果在循环里创建一个不会改变的表,你可以把它挪到循环外面,甚至移到函数外作为上值。试对比:

Lua性能优化技巧
function foo (...)
     for i = 1, n do
          local t = {1, 2, 3, "hi"}
          -- 做一些不会改变t表的事情
          --...
     end
end
Lua性能优化技巧

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

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

Lua性能优化技巧
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
Lua性能优化技巧

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

Lua性能优化技巧
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
Lua性能优化技巧

但是,我们不能把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中,利用高阶函数,我们可以定义一个通用的缓存化函数:

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
Lua性能优化技巧

对于任何函数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周期,但是它会降低程序的内存消耗总量,并可能因此减少分页。只有谨慎地测试才能给你最佳的参数值。

 

正如我们在前言里所说,优化是一个技巧性很强的工作,从程序是否需要优化开始,有若干个方面的内容需要考量。如果程序真的有性能问题,那么我们应该将精力集中于优化哪里和如何优化。

我们在这里讨论的技巧既不是唯一的,也不是最重要的方面。我们在这里专注于讨论专门针对Lua的优化方式,因为有很多其他的方式可以了解通用的程序优化技巧。

在本文结束之前,我还想介绍两种从更大的尺度上优化Lua程序性能的方式,但是它们都牵涉到Lua代码之外的修改。第一个是使用LuaJIT[1],一个Lua的即时编译器,由Mike Pall开发。他所作的工作非常卓越,而且LuaJIT可能是所有动态语言里最快的JIT了。使用它的代价是它只能在x86架构上运行,而且你需要一个非标准的Lua解释器(LuaJIT)来运行你的程序。所获得的好处是你可以在不修改代码的情况下让程序的运行速度提高到原先的5倍。第二个方式是将部分代码移到C中实现。这一条的重点在于为C代码选择合适的粒度。一方面,如果你把一些非常简单的函数移动到C里,Lua和C之间的通讯开销会抵消使用C编写函数带来的性能优势;另一方面,如果你把太大的函数移到C里,你又失去了Lua所提供的灵活性。最后,还要注意的是这两种方式有时候是不兼容的。你把越多的代码移到C里,LuaJIT所能带来的优化就越少。