编写高性能的Lua代码
编写高性能的Lua代码
Comments
前言
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
ADD001
但是若a和b都没有声明为local变量,则预编译会产生如下指令:
GETGLOBAL 00 ;get a
GETGLOBAL 11 ;get b
ADD 001 ;do add
SETGLOBAL 00 ;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]
触发了一次rehash,Lua将数组部分的长度设置为
= true2^0
,即1,哈希部分仍为空。在第二次迭代中,a[2]
再次触发了rehash,将数组部分长度设为
= true2^1
,即2。最后一次迭代,又触发了一次rehash,将数组部分长度设为2^2
,即4。
下面这段代码:
a={}
a.x=1;a.y=2;a.z=3
与上一段代码类似,只是其触发了三次表中哈希部分的rehash而已。
只有三个元素的表,会执行三次rehash;然而有一百万个元素的表仅仅只会执行20次rehash而已,因为2^20
。但是,如果你创建了非常多的长度很小的表(比如坐标点:
= 1048576 > 1000000point
),这可能会造成巨大的影响。
= {x=0,y=0}
如果你有很多非常多的很小的表需要创建时,你可以将其预先填充以避免rehash。比如:{true,true,true}
,Lua知道这个表有三个元素,所以Lua直接创建了三个元素长度的数组。类似的,{x=1,
,Lua会在其哈希部分中创建长度为4的数组。
y=2, z=3}
以下代码执行时间为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的buffer整个的复制到$x的buffer中,当字符串很长时,这个操作的代价将十分昂贵。而在Lua,同样的赋值,只复制引用,十分的高效。
= $y
但是只保存引用会降低在字符串连接时的速度。在Perl中,$s
和
= $s . 'x'$s
的效率差距惊人。前者,将会获取整个$s的拷贝,并将’x’添加到它的末尾;而后者,将直接将’x’插入到$x的buffer末尾。
.= 'x'
由于后者不需要进行拷贝,所以其效率和$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上的辛勤劳动和付出!
编写高性能的Lua代码的更多相关文章
- [转]编写高性能的Lua代码
昨天晚上闲来无事,看室友在电脑上挂机玩游戏,用的一个辅助脚本,以为是lua写的脚本在跑,实际调查发现是按键精灵的脚本. 于是在网上找相关Lua开发游戏脚本的案例,看到一个人的博客,内容很不错,学到了很 ...
- 编写高性能的 Lua 代码
前言 Lua是一门以其性能著称的脚本语言,被广泛应用在很多方面,尤其是游戏.像<魔兽世界>的插件,手机游戏<大掌门><神曲><迷失之地>等都是用Lua来 ...
- 程序员笔记|如何编写高性能的Java代码
一.并发 Unable to create new native thread …… 问题1:Java中创建一个线程消耗多少内存? 每个线程有独自的栈内存,共享堆内存 问题2:一台机器可以创建多少线程 ...
- 编写高性能的jQuery代码
jQuery Optimization 现在jQuery已经出现在很多项目中,然而许多同学忽略了他的性能问题以及代码质量问题, 下面是我对jQuery的一些性能方面的学习. 选择器 选择器是jQuer ...
- 编写高性能的javascript代码(持续更新)
参考资料: Vanilla JS——世界上最轻量的JavaScript框架(没有之一) http://segmentfault.com/a/1190000000355277 探索高效jQuery的奥秘 ...
- 编写高性能Javascript代码的若干建议
多年来,Javascript一直在web应用开发中占据重要的地位,但是很多开发者往往忽视一些性能方面的知识,特别是随着计算机硬件的不断升级,开发者越发觉得Javascript性能优化的好不好对网页的执 ...
- 编写高性能Javascript
编写高性能Javascript 多年来,Javascript一直在web应用开发中占据重要的地位,但是很多开发者往往忽视一些性能方面的知识,特别是随着计算机硬件的不断升级,开发者越发觉得Javascr ...
- cocos2dx android运行Luac编译后的lua代码
环境: win7 64 cocos2d-2.1rc0-x-2.1.2 lua 5.1 通常我们编写好的lua代码都是明文形式,谁都可以查看修改,为了防止自己的劳动成果不被别人轻易的盗取,可以使用lua ...
- Openresty编写Lua代码一例
1.前段时间纠结了很久,一直弄不清lua和tomcat的联系.一直认为是lua调用tomcat的接口才可使用,后面才明白过来,进入了一个误区,lua本身就是一门独立的脚本语言.在openresty里面 ...
随机推荐
- Jenkins的安装
安装环境: 512M内存 10G硬盘空间 Java 8环境 先来创建jenkins的运行目录: mkdir /data/jenkins && cd /data/jenkins 下载je ...
- [LeetCode] K Inverse Pairs Array K个翻转对数组
Given two integers n and k, find how many different arrays consist of numbers from 1 to n such that ...
- Mac 下安装 MySQL 经历
1.使用 homebrew 安装: brew install mysql 结果报错: $ brew install mysql ==> Downloading http://dev.mysql. ...
- [FJOI 2014]最短路径树问题
Description 给一个包含n个点,m条边的无向连通图.从顶点1出发,往其余所有点分别走一次并返回. 往某一个点走时,选择总长度最短的路径走.若有多条长度最短的路径,则选择经过的顶点序列字典序最 ...
- 【NOIP2012-开车旅行】
这道题:你不仅要学会两人交换开车,还要做到高效驾驶. ·分析: 在拨开花哨题目的迷雾之后,发现两个重要突破口: ①从每个点开始,他们的路径是一定的,不存在决策选取. ...
- 51Nod 1331 狭窄的通道
有一个长为L的狭窄通道,我们假设这个通道在x轴上,其两个出口分别在x=0与x=L处.在这个通道里有N只狼,第i只狼有一个初始位置ai,它想到达位置bi(0<=i=L处空间足够大可以装下任意数量的 ...
- 【Codeforces 851D Arpa and a list of numbers】
Arpa的数列要根据GCD变成好数列. ·英文题,述大意: 给出一个长度为n(n<=5000000)的序列,其中的元素a[i]<=106,然后输入两个数x,y(x,y<=1 ...
- 勤拂拭软件系列教程 - java web开发
勤拂拭软件工作室持续推出Java Web开发系列教程与案例,供广大朋友分享交流技术经验,帮助喜欢java的朋友们学习进步: 1. 勤拂拭软件 java web 开发教程(1) - 开发环境搭建 勤拂拭 ...
- Angular中 build的时候遇到的错误--There are multiple modules with names that only differ in casing
今天早上遇到一个Angular的编译的时候的错误 具体信息: There are multiple modules with names that only differ in casing.This ...
- C#中Fun简单介绍及运用到项目中与缓存(本地缓存,Redis)结合使用
1.简单介绍Fun C#中Fun和Action有点类似,都是一个委托方法,不同的是Func是有返回值的,而Action没有. (T)此委托封装的方法的参数类型. 备注:详情了解Fun到(https: ...