编写高性能Lua代码的方法_Lua

前言

Lua是一门以其性能著称的脚本语言,被广泛应用在很多方面,尤其是游戏。像《魔兽世界》的插件,手机游戏《大掌门》《神曲》《迷失之地》等都是用Lua来写的逻辑。

所以大部分时候我们不需要去考虑性能问题。Knuth有句名言:“过早优化是万恶之源”。其意思就是过早优化是不必要的,会浪费大量时间,而且容易导致代码混乱。

所以一个好的程序员在考虑优化性能前必须问自己两个问题:“我的程序真的需要优化吗?”。如果答案为是,那么再问自己:“优化哪个部分?”。

我们不能靠臆想和凭空猜测来决定优化哪个部分,代码的运行效率必须是可测量的。我们需要借助于分析器来测定性能的瓶颈,然后着手优化。优化后,我们仍然要借助于分析器来测量所做的优化是否真的有效。

我认为最好的方式是在首次编写的时候按照最佳实践去写出高性能的代码,而不是编写了一堆垃圾代码后,再考虑优化。相信工作后大家都会对事后的优化的繁琐都深有体会。

一旦你决定编写高性能的Lua代码,下文将会指出在Lua中哪些代码是可以优化的,哪些代码会是运行缓慢的,然后怎么去优化它们。

使用local

在代码运行前,Lua会把源码预编译成一种中间码,类似于Java的虚拟机。这种格式然后会通过C的解释器进行解释,整个过程其实就是通过一个while循环,里面有很多的switch...case语句,一个case对应一条指令来解析。

自Lua 5.0之后,Lua采用了一种类似于寄存器的虚拟机模式。Lua用栈来储存其寄存器。每一个活动的函数,Lua都会其分配一个栈,这个栈用来储存函数里的活动记录。每一个函数的栈都可以储存至多250个寄存器,因为栈的长度是用8个比特表示的。

有了这么多的寄存器,Lua的预编译器能把所有的local变量储存在其中。这就使得Lua在获取local变量时其效率十分的高。

举个栗子: 假设a和b为local变量,a = a + b的预编译会产生一条指令:

复制代码 代码如下:

;a是寄存器0 b是寄存器1
ADD 0 0 1

但是若a和b都没有声明为local变量,则预编译会产生如下指令:

复制代码 代码如下:

GETGLOBAL    0 0    ;get a
GETGLOBAL    1 1    ;get b
ADD          0 0 1  ;do add
SETGLOBAL    0 0    ;set a

所以你懂的:在写Lua代码时,你应该尽量使用local变量。

以下是几个对比测试,你可以复制代码到你的编辑器中,进行测试。

复制代码 代码如下:

a = os.clock()
for i = 1,10000000 do
  local x = math.sin(i)
end
b = os.clock()
print(b-a) -- 1.113454

把math.sin赋给local变量sin:

复制代码 代码如下:

a = os.clock()
local sin = math.sin
for i = 1,10000000 do
  local x = sin(i)
end
b = os.clock()
print(b-a) --0.75951

直接使用math.sin,耗时1.11秒;使用local变量sin来保存math.sin,耗时0.76秒。可以获得30%的效率提升!

关于表(table)

表在Lua中使用十分频繁,因为表几乎代替了Lua的所有容器。所以快速了解一下Lua底层是如何实现表,对我们编写Lua代码是有好处的。

Lua的表分为两个部分:数组(array)部分和哈希(hash)部分。数组部分包含所有从1到n的整数键,其他的所有键都储存在哈希部分中。

哈希部分其实就是一个哈希表,哈希表本质是一个数组,它利用哈希算法将键转化为数组下标,若下标有冲突(即同一个下标对应了两个不同的键),则它会将冲突的下标上创建一个链表,将不同的键串在这个链表上,这种解决冲突的方法叫做:链地址法。

当我们把一个新键值赋给表时,若数组和哈希表已经满了,则会触发一个再哈希(rehash)。再哈希的代价是高昂的。首先会在内存中分配一个新的长度的数组,然后将所有记录再全部哈希一遍,将原来的记录转移到新数组中。新哈希表的长度是最接近于所有元素数目的2的乘方。

当创建一个空表时,数组和哈希部分的长度都将初始化为0,即不会为它们初始化任何数组。让我们来看下执行下面这段代码时在Lua中发生了什么:

复制代码 代码如下:

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

最开始,Lua创建了一个空表a,在第一次迭代中,a[1] = true触发了一次rehash,Lua将数组部分的长度设置为2^0,即1,哈希部分仍为空。在第二次迭代中,a[2] = true再次触发了rehash,将数组部分长度设为2^1,即2。最后一次迭代,又触发了一次rehash,将数组部分长度设为2^2,即4。

下面这段代码:

复制代码 代码如下:

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

与上一段代码类似,只是其触发了三次表中哈希部分的rehash而已。

只有三个元素的表,会执行三次rehash;然而有一百万个元素的表仅仅只会执行20次rehash而已,因为2^20 = 1048576 > 1000000。但是,如果你创建了非常多的长度很小的表(比如坐标点:point = {x=0,y=0}),这可能会造成巨大的影响。

如果你有很多非常多的很小的表需要创建时,你可以将其预先填充以避免rehash。比如:{true,true,true},Lua知道这个表有三个元素,所以Lua直接创建了三个元素长度的数组。类似的,{x=1, y=2, z=3},Lua会在其哈希部分中创建长度为4的数组。

以下代码执行时间为1.53秒:

复制代码 代码如下:

a = os.clock()
for i = 1,2000000 do
    local a = {}
    a[1] = 1; a[2] = 2; a[3] = 3
end
b = os.clock()
print(b-a)  --1.528293

如果我们在创建表的时候就填充好它的大小,则只需要0.75秒,一倍的效率提升!

复制代码 代码如下:

a = os.clock()
for i = 1,2000000 do
    local a = {1,1,1}
    a[1] = 1; a[2] = 2; a[3] = 3
end
b = os.clock()
print(b-a)  --0.746453

所以,当需要创建非常多的小size的表时,应预先填充好表的大小。

关于字符串

与其他主流脚本语言不同的是,Lua在实现字符串类型有两方面不同。

第一,所有的字符串在Lua中都只储存一份拷贝。当新字符串出现时,Lua检查是否有其相同的拷贝,若没有则创建它,否则,指向这个拷贝。这可以使得字符串比较和表索引变得相当的快,因为比较字符串只需要检查引用是否一致即可;但是这也降低了创建字符串时的效率,因为Lua需要去查找比较一遍。

第二,所有的字符串变量,只保存字符串引用,而不保存它的buffer。这使得字符串的赋值变得十分高效。例如在Perl中,$x = $y,会将$y的buffer整个的复制到$x的buffer中,当字符串很长时,这个操作的代价将十分昂贵。而在Lua,同样的赋值,只复制引用,十分的高效。

但是只保存引用会降低在字符串连接时的速度。在Perl中,$s = $s . 'x'和$s .= 'x'的效率差距惊人。前者,将会获取整个$s的拷贝,并将'x'添加到它的末尾;而后者,将直接将'x'插入到$x的buffer末尾。

由于后者不需要进行拷贝,所以其效率和$s的长度无关,因为十分高效。

在Lua中,并不支持第二种更快的操作。以下代码将花费6.65秒:

复制代码 代码如下:

a = os.clock()
local s = ''
for i = 1,300000 do
    s = s .. 'a'
end
b = os.clock()
print(b-a)  --6.649481

我们可以用table来模拟buffer,下面的代码只需花费0.72秒,9倍多的效率提升:

复制代码 代码如下:

a = os.clock()
local s = ''
local t = {}
for i = 1,300000 do
    t[#t + 1] = 'a'
end
s = table.concat( t, '')
b = os.clock()
print(b-a)  --0.07178

所以:在大字符串连接中,我们应避免..。应用table来模拟buffer,然后concat得到最终字符串。

3R原则

3R原则(the rules of 3R)是:减量化(reducing),再利用(reusing)和再循环(recycling)三种原则的简称。

3R原则本是循环经济和环保的原则,但是其同样适用于Lua。

Reducing

有许多办法能够避免创建新对象和节约内存。例如:如果你的程序中使用了太多的表,你可以考虑换一种数据结构来表示。

举个栗子。 假设你的程序中有多边形这个类型,你用一个表来储存多边形的顶点:

复制代码 代码如下:

polyline = {
    { x = 1.1, y = 2.9 },
    { x = 1.1, y = 3.7 },
    { x = 4.6, y = 5.2 },
    ...
}

以上的数据结构十分自然,便于理解。但是每一个顶点都需要一个哈希部分来储存。如果放置在数组部分中,则会减少内存的占用:

复制代码 代码如下:

polyline = {
    { 1.1, 2.9 },
    { 1.1, 3.7 },
    { 4.6, 5.2 },
    ...
}

一百万个顶点时,内存将会由153.3MB减少到107.6MB,但是代价是代码的可读性降低了。

最变态的方法是:

复制代码 代码如下:

polyline = {
    x = {1.1, 1.1, 4.6, ...},
    y = {2.9, 3.7, 5.2, ...}
}

一百万个顶点,内存将只占用32MB,相当于原来的1/5。你需要在性能和代码可读性之间做出取舍。

在循环中,我们更需要注意实例的创建。

复制代码 代码如下:

for i=1,n do
    local t = {1,2,3,'hi'}
    --执行逻辑,但t不更改
    ...
end

我们应该把在循环中不变的东西放到循环外来创建:

复制代码 代码如下:

local t = {1,2,3,'hi'}
for i=1,n do
    --执行逻辑,但t不更改
    ...
end

Reusing

如果无法避免创建新对象,我们需要考虑重用旧对象。

考虑下面这段代码:

复制代码 代码如下:

local t = {}
for i = 1970, 2000 do
    t[i] = os.time({year = i, month = 6, day = 14})
end

在每次循环迭代中,都会创建一个新表{year = i, month = 6, day = 14},但是只有year是变量。

下面这段代码重用了表:

复制代码 代码如下:

local t = {}
local aux = {year = nil, month = 6, day = 14}
for i = 1970, 2000 do
    aux.year = i;
    t[i] = os.time(aux)
end

另一种方式的重用,则是在于缓存之前计算的内容,以避免后续的重复计算。后续遇到相同的情况时,则可以直接查表取出。这种方式实际就是动态规划效率高的原因所在,其本质是用空间换时间。

Recycling

Lua自带垃圾回收器,所以我们一般不需要考虑垃圾回收的问题。

了解Lua的垃圾回收能使得我们编程的自由度更大。

Lua的垃圾回收器是一个增量运行的机制。即回收分成许多小步骤(增量的)来进行。

频繁的垃圾回收可能会降低程序的运行效率。

我们可以通过Lua的collectgarbage函数来控制垃圾回收器。

collectgarbage函数提供了多项功能:停止垃圾回收,重启垃圾回收,强制执行一次回收循环,强制执行一步垃圾回收,获取Lua占用的内存,以及两个影响垃圾回收频率和步幅的参数。

对于批处理的Lua程序来说,停止垃圾回收collectgarbage("stop")会提高效率,因为批处理程序在结束时,内存将全部被释放。

对于垃圾回收器的步幅来说,实际上很难一概而论。更快幅度的垃圾回收会消耗更多CPU,但会释放更多内存,从而也降低了CPU的分页时间。只有小心的试验,我们才知道哪种方式更适合。

结语

我们应该在写代码时,按照高标准去写,尽量避免在事后进行优化。

如果真的有性能问题,我们需要用工具量化效率,找到瓶颈,然后针对其优化。当然优化过后需要再次测量,查看是否优化成功。

在优化中,我们会面临很多选择:代码可读性和运行效率,CPU换内存,内存换CPU等等。需要根据实际情况进行不断试验,来找到最终的平衡点。

最后,有两个终极武器:

第一、使用LuaJIT,LuaJIT可以使你在不修改代码的情况下获得平均约5倍的加速。查看LuaJIT在x86/x64下的性能提升比。

第二、将瓶颈部分用C/C++来写。因为Lua和C的天生近亲关系,使得Lua和C可以混合编程。但是C和Lua之间的通讯会抵消掉一部分C带来的优势。

注意:这两者并不是兼容的,你用C改写的Lua代码越多,LuaJIT所带来的优化幅度就越小。

声明

这篇文章是基于Lua语言的创造者Roberto Ierusalimschy在Lua Programming Gems 中的Lua Performance Tips翻译改写而来。本文没有直译,做了许多删节,可以视为一份笔记。

感谢Roberto在Lua上的辛勤劳动和付出!

时间: 2024-11-02 15:43:03

编写高性能Lua代码的方法_Lua的相关文章

Lua中计算、执行字符串中Lua代码的方法_Lua

一.Lua中执行字符串 运行过程中有个问题,我有个字符串,是一个数学表达式,如何计算这个字符串表达式的值呢? 比如,local param = "7*100", 我需要的结果其实是700,但是怎么样直接计算出这个值呢?方法如下 字符串前面 加个 "return" 然后loadstring以后得到一个function 然后执行获得700的返回值,这样通过转化,得到的结果如下: 二.以字符串形式执行Lua代码 有时候,我们在代码中希望能够动态的切换上下文,改变程序的处理

Lua中计算、执行字符串中Lua代码的方法

  这篇文章主要介绍了Lua中计算.执行字符串中Lua代码的方法,类似JavaScript中eval函数的功能,在Lua中也可以实现,需要的朋友可以参考下 一.Lua中执行字符串 运行过程中有个问题,我有个字符串,是一个数学表达式,如何计算这个字符串表达式的值呢? 比如,local param = "7*100", 我需要的结果其实是700,但是怎么样直接计算出这个值呢?方法如下 字符串前面 加个 "return" 然后loadstring以后得到一个functio

实例探究Python以并发方式编写高性能端口扫描器的方法

来源:http://www.jb51.net/article/86615.htm 关于端口扫描器端口扫描工具(Port Scanner)指用于探测服务器或主机开放端口情况的工具.常被计算机管理员用于确认安全策略,同时被攻击者用于识别目标主机上的可运作的网络服务. 端口扫描定义是客户端向一定范围的服务器端口发送对应请求,以此确认可使用的端口.虽然其本身并不是恶意的网络活动,但也是网络攻击者探测目标主机服务,以利用该服务的已知漏洞的重要手段.端口扫描的主要用途仍然只是确认远程机器某个服务的可用性.

使用Lua来扩展C++程序的方法_Lua

 介绍 如果用户能够通过一些脚本语言来修改应用本身的行为,那么许多应用可以变得更适合用户使用.一些商业应用就提供了此类便利.例如 Microsoft Office 的 VBA 脚本编程或在视频游戏 World of Warcraft 中使用 Lua .脚本语言把应用作为一个平台提供一系列终端用户可以获得并操控的服务. 做为嵌入到程序中的语言,我们有很多可用的选择:开源和不开源的脚本引擎,或者可以从头开始创建一个.现在,最为熟知的脚本语言是JavaScript,Lua和Python,还有很多其它的

编写高质量代码改善C#程序的157个建议[为泛型指定初始值、使用委托声明、使用Lambda替代方法和匿名方法]

原文:编写高质量代码改善C#程序的157个建议[为泛型指定初始值.使用委托声明.使用Lambda替代方法和匿名方法] 前言 泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能.基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用.同时,它减少了泛型类及泛型方法中的转型,确保了类型安全.委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用.事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分.一旦我们

Lua中让回调函数支持回调对象方法的解决方法_Lua

在 Cocos2d-Lua 中,存在很多异步或延迟的操作,例如后台加载图片.等待一定时间执行代码等.这些功能的函数通常要求传入一个 function 作为参数. 复制代码 代码如下: -- 在后台加载一个图像,加载完成后输出消息 display.addImageAsync("hello.png", function()     print("load hello.png completed") end) 但如果我们希望这种回调支持一个对象方法,就有点小困难了.因为

CSS开发框架技术OOCSS编写和管理CSS的方法

文章简介:今天最流行的CSS开发框架技术当属OOCSS,尽管还有其他类似的技术存在,如BEM.这些方法试图对CSS采用面向对象的编程原则.尽管样式语言和面向对象的软件设计原则在概念之间存在一定的问题,这些微妙的东西对于一个欠缺经验的开发人员来说可能不会立即显现出来.最令人不 公认的拥有一个编写和管理CSS的方法比什么都要更好.尽管如此,一些开发人员的实践是不利于语义化质量和长期的可维护性.我们要讨论一些被提倡的"CSS框架方法"的问题和作为Web开发人员,我们如何可以更好的解决这些问题

编写高性能 Web 应用程序的10个技巧

web|程序|技巧|性能|技巧|性能 本文讨论:常见的 ASP.NET 性能神话 有用的 ASP.NET 性能技巧和诀窍 在 ASP.NET 中处理数据库的一些建议 缓冲以及用 ASP.NET 进行后台处理 本文使用下列技术:ASP.NET,.NET 框架,IIS 用 ASP.NET 编写 Web 应用程序其轻松程度令人难以置信.它是如此的容易,以至于许多开发人员不用花费多少时间来构筑其应用便能获得非常好的性能.在本文中,我将给出10个编写高性能 Web 应用的技巧.我的评论不仅仅局限与 ASP

编写高性能 Web 应用程序的10 个技巧

web|程序|技巧|性能|技巧|性能 技巧 作者:Rob Howard 本文讨论 • 常见 ASP.NET 性能难点 • 面向 ASP.NET 的有用性能提示和技巧 • 在 ASP.NET 中使用数据库的建议 • 使用 ASP.NET 进行缓存和后台处理 本文使用了以下技术:ASP.NET..NET Framework 和 IIS 本页内容 数据层性能 技巧 1 - 返回多个结果集 技巧 2 - 分页的数据访问 技巧 3 - 连接池 技巧 4 - ASP.NET 缓存 API 技巧 5 - 每请