免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 8799 | 回复: 13
打印 上一主题 下一主题

[技术动态] 转-编写高性能的Lua代码 [复制链接]

论坛徽章:
36
子鼠
日期:2013-08-28 22:23:29黄金圣斗士
日期:2015-12-01 11:37:51程序设计版块每日发帖之星
日期:2015-12-14 06:20:00CU十四周年纪念徽章
日期:2015-12-22 16:50:40IT运维版块每日发帖之星
日期:2016-01-25 06:20:0015-16赛季CBA联赛之深圳
日期:2016-01-27 10:31:172016猴年福章徽章
日期:2016-02-18 15:30:3415-16赛季CBA联赛之福建
日期:2016-04-07 11:25:2215-16赛季CBA联赛之青岛
日期:2016-04-29 18:02:5915-16赛季CBA联赛之北控
日期:2016-06-20 17:38:50技术图书徽章
日期:2016-07-19 13:54:03程序设计版块每日发帖之星
日期:2016-08-21 06:20:00
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2014-04-19 20:19 |只看该作者 |倒序浏览
from: http://wuzhiwei.net/lua_performance/


前言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的预编译会产生一条指令:
                                                                                                               
                                        1
2

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

                       
               

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

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

                       
               

所以你懂的:在写Lua代码时,你应该尽量使用local变量
以下是几个对比测试,你可以复制代码到你的编辑器中,进行测试。
                                                                                                               
                                        1
2
3
4
5
6

                               
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:
                                                                                                               
                                        1
2
3
4
5
6
7

                               
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中发生了什么:
                                                                                                               
                                        1
2
3
4

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

                       
               

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

                               
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秒:
                                                                                                               
                                        1
2
3
4
5
6
7

                               
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秒,一倍的效率提升!
                                                                                                               
                                        1
2
3
4
5
6
7

                               
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秒:
                                                                                                               
                                        1
2
3
4
5
6
7

                               
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倍多的效率提升:
                                                                                                               
                                        1
2
3
4
5
6
7
8
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有许多办法能够避免创建新对象和节约内存。例如:如果你的程序中使用了太多的表,你可以考虑换一种数据结构来表示。
举个栗子。假设你的程序中有多边形这个类型,你用一个表来储存多边形的顶点:
                                                                                                               
                                        1
2
3
4
5
6

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

                       
               

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

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

                       
               

一百万个顶点时,内存将会由153.3MB减少到107.6MB,但是代价是代码的可读性降低了。
最变态的方法是:
                                                                                                               
                                        1
2
3
4

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

                       
               

一百万个顶点,内存将只占用32MB,相当于原来的1/5。你需要在性能和代码可读性之间做出取舍。
在循环中,我们更需要注意实例的创建。
                                                                                                               
                                        1
2
3
4
5

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

                       
               

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

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

                       
               

Reusing如果无法避免创建新对象,我们需要考虑重用旧对象。
考虑下面这段代码:
                                                                                                               
                                        1
2
3
4

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

                       
               

在每次循环迭代中,都会创建一个新表{year = i, month = 6, day = 14},但是只有year是变量。
下面这段代码重用了表:
                                                                                                               
                                        1
2
3
4
5
6

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

                       
               

另一种方式的重用,则是在于缓存之前计算的内容,以避免后续的重复计算。后续遇到相同的情况时,则可以直接查表取出。这种方式实际就是动态规划效率高的原因所在,其本质是用空间换时间。
RecyclingLua自带垃圾回收器,所以我们一般不需要考虑垃圾回收的问题。
了解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上的辛勤劳动和付出!

论坛徽章:
36
子鼠
日期:2013-08-28 22:23:29黄金圣斗士
日期:2015-12-01 11:37:51程序设计版块每日发帖之星
日期:2015-12-14 06:20:00CU十四周年纪念徽章
日期:2015-12-22 16:50:40IT运维版块每日发帖之星
日期:2016-01-25 06:20:0015-16赛季CBA联赛之深圳
日期:2016-01-27 10:31:172016猴年福章徽章
日期:2016-02-18 15:30:3415-16赛季CBA联赛之福建
日期:2016-04-07 11:25:2215-16赛季CBA联赛之青岛
日期:2016-04-29 18:02:5915-16赛季CBA联赛之北控
日期:2016-06-20 17:38:50技术图书徽章
日期:2016-07-19 13:54:03程序设计版块每日发帖之星
日期:2016-08-21 06:20:00
2 [报告]
发表于 2014-04-19 20:22 |只看该作者
本帖最后由 cokeboL 于 2014-04-19 20:23 编辑

用惯了lua,越来越感觉在大部分地方可以替代c++(至少在我们行业里),项目里添加lua,然后使用tolua、luabriage之类的,用上luajit
然后,什么c11、boost,都不用搞了,世界多美美丽,世界多么轻松

论坛徽章:
1
2015年辞旧岁徽章
日期:2015-03-03 16:54:15
3 [报告]
发表于 2014-04-19 20:52 |只看该作者
尴尬,很多时候不再需要C/C++提供的高性能

论坛徽章:
36
子鼠
日期:2013-08-28 22:23:29黄金圣斗士
日期:2015-12-01 11:37:51程序设计版块每日发帖之星
日期:2015-12-14 06:20:00CU十四周年纪念徽章
日期:2015-12-22 16:50:40IT运维版块每日发帖之星
日期:2016-01-25 06:20:0015-16赛季CBA联赛之深圳
日期:2016-01-27 10:31:172016猴年福章徽章
日期:2016-02-18 15:30:3415-16赛季CBA联赛之福建
日期:2016-04-07 11:25:2215-16赛季CBA联赛之青岛
日期:2016-04-29 18:02:5915-16赛季CBA联赛之北控
日期:2016-06-20 17:38:50技术图书徽章
日期:2016-07-19 13:54:03程序设计版块每日发帖之星
日期:2016-08-21 06:20:00
4 [报告]
发表于 2014-04-19 21:39 |只看该作者
回复 3# 群雄逐鹿中原


尤其是这种东西以后普及,硬件越来越牛逼了

http://blog.sina.com.cn/s/blog_555cddff0100gt9i.html

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
5 [报告]
发表于 2014-04-19 23:51 |只看该作者
对的,完全这么认为,Lua+C基本上完全足够了的说。

对于绑定,如果希望能简单地去写更好的绑定,可以参看我的lbind项目,目前是很初级的阶段,但是runtime里面的lbind.h和lbind.c已经可以用了(我在很多个项目里面用过了),其实就是一个Lua C API的扩展API,提供了很多很方便的功能(比如快捷操作metatable,操作module,快速+快捷绑定类型等等)。

这个API的类型系统是全新的,按照Luiz的说法,在Lua5.2里面检查类型的速度可以提高20%左右。

地址在:http://github.com/starwing/lbind

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
6 [报告]
发表于 2014-04-20 00:15 |只看该作者
对于LuaJIT,插一句嘴。

通常情况是这样的:LuaJIT对C module的容忍性很低,以至于,如果你的代码是在C module的基础上完成的(比如常见的游戏引擎),那么很遗憾的,LuaJIT不仅不能增加速度,反而可能会降低(我们以前的经验是这样的)。

遇到这个问题的话,你有两个解决方案:
1. 关掉JIT,这样可以节省掉trace abort的时间,LuaJIT纯汇编手写的解释器仍然比Lua的快,不过这时速度差距可能就只有一倍左右了。
2. 用LuaJIT的方法(CFFI),重新写一份绑定,以Lua为中心。

第二种方法的工作量其实还是蛮大的,而且有一些坑。

不过最新版本的LuaJIT (2.1 git head)拥有了一个新特性:在执行C函数的时候,不会再abort掉trace了,这让JIT的速度有一定的回升,具体的数量待定。

其实我自己已经很长时间没使用LuaJIT了,主要是其宣称的高速全新的“四色垃圾回收”一直没有实现,另外就是,LuaJIT已经在Lua5.1上面暂停了很长时间了。很多时候,我宁愿用C API + Lua5.2的方法写代码,目前看来效果也不错。

通讯的问题,上文的lbind能解决一点,但是还是会有很多检查,这个无可避免,我可以提一些方法,但是也需要根据情况来使用:
1. DSL:在你的绑定里面设计子语言,你自己解析,然后产生对象。这种方案能减少C和Lua的交互,可以把复杂的逻辑一次性递交给C,然后就让C干活去就行了。
2. 在C里面pushstring的代价相对来说是最高昂的(其次是set/gettable),尽量避免这样的操作。和C交互数字的速度是很快的,但是小心Lua可能会对字符串执行数字操作(字符串-数字自动转换),所以你需要很清楚你想要的到底是什么。
3. 清晰简单,粒度合理的接口也是一个重点。

反正这是需要经验的事情,做好了会用的很舒服,不过那需要大量的时间来练习的说。多想多做吧。

论坛徽章:
12
巳蛇
日期:2013-09-16 15:32:242015年辞旧岁徽章
日期:2015-03-03 16:54:152015年亚洲杯之约旦
日期:2015-02-11 14:38:37双鱼座
日期:2015-01-05 11:05:47戌狗
日期:2014-12-08 09:41:18戌狗
日期:2014-08-15 09:29:29双子座
日期:2014-08-05 09:17:17卯兔
日期:2014-06-08 15:32:18巳蛇
日期:2014-01-27 08:47:08白羊座
日期:2013-11-28 21:04:15巨蟹座
日期:2013-11-13 21:58:012015年亚洲杯之科威特
日期:2015-04-17 16:51:51
7 [报告]
发表于 2014-04-20 17:25 |只看该作者
原文中:
  1. 一百万个顶点时,内存将会由95KB减少到65KB,但是代价是代码的可读性降低了。
复制代码
算错了吧,不然怎么存的?

论坛徽章:
36
子鼠
日期:2013-08-28 22:23:29黄金圣斗士
日期:2015-12-01 11:37:51程序设计版块每日发帖之星
日期:2015-12-14 06:20:00CU十四周年纪念徽章
日期:2015-12-22 16:50:40IT运维版块每日发帖之星
日期:2016-01-25 06:20:0015-16赛季CBA联赛之深圳
日期:2016-01-27 10:31:172016猴年福章徽章
日期:2016-02-18 15:30:3415-16赛季CBA联赛之福建
日期:2016-04-07 11:25:2215-16赛季CBA联赛之青岛
日期:2016-04-29 18:02:5915-16赛季CBA联赛之北控
日期:2016-06-20 17:38:50技术图书徽章
日期:2016-07-19 13:54:03程序设计版块每日发帖之星
日期:2016-08-21 06:20:00
8 [报告]
发表于 2014-04-20 18:44 |只看该作者
回复 7# zhaohongjian000


    table的hash部分维护hash结构的内存比数组结构要多

论坛徽章:
36
子鼠
日期:2013-08-28 22:23:29黄金圣斗士
日期:2015-12-01 11:37:51程序设计版块每日发帖之星
日期:2015-12-14 06:20:00CU十四周年纪念徽章
日期:2015-12-22 16:50:40IT运维版块每日发帖之星
日期:2016-01-25 06:20:0015-16赛季CBA联赛之深圳
日期:2016-01-27 10:31:172016猴年福章徽章
日期:2016-02-18 15:30:3415-16赛季CBA联赛之福建
日期:2016-04-07 11:25:2215-16赛季CBA联赛之青岛
日期:2016-04-29 18:02:5915-16赛季CBA联赛之北控
日期:2016-06-20 17:38:50技术图书徽章
日期:2016-07-19 13:54:03程序设计版块每日发帖之星
日期:2016-08-21 06:20:00
9 [报告]
发表于 2014-04-20 18:47 |只看该作者
回复 6# starwing83


    lua5.2我还没用过的说

    师傅对nodejs熟不?我想了解下v8的效率怎么样,还是比lua要差的吧?

论坛徽章:
12
巳蛇
日期:2013-09-16 15:32:242015年辞旧岁徽章
日期:2015-03-03 16:54:152015年亚洲杯之约旦
日期:2015-02-11 14:38:37双鱼座
日期:2015-01-05 11:05:47戌狗
日期:2014-12-08 09:41:18戌狗
日期:2014-08-15 09:29:29双子座
日期:2014-08-05 09:17:17卯兔
日期:2014-06-08 15:32:18巳蛇
日期:2014-01-27 08:47:08白羊座
日期:2013-11-28 21:04:15巨蟹座
日期:2013-11-13 21:58:012015年亚洲杯之科威特
日期:2015-04-17 16:51:51
10 [报告]
发表于 2014-04-20 18:51 |只看该作者
cokeboL 发表于 2014-04-20 18:44
回复 7# zhaohongjian000


我不是说太多了。就按一个顶点一个字节算,100万也得1M的空间啊。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP