Lua·001·性能优化

Lua脚本是C语言实现的脚本,广泛应用于客户端扩展脚本,例如魔兽世界等网游。但是Lua的性能一般,并且有许多不好的实现,误用会大大降低系统的性能。
网络上有一些关于Lua脚本性能优化的资料,但是都是针对Lua撰写的,写作年代较早,一些优化技巧不完全正确,而且没有针对LuaJIT优化过后的代码进行考虑。
本章对于Lua的一些语法,在Lua和LuaJIT中进行比较测试,并给出相关优化数据和结论。

由于LuaJIT的性能较Lua有很大的提高,在测试时使用的循环次数不同,以避免时间太短导致测量不准确。
在结果中,In LuaJIT (100x)表示LuaJIT中执行性能但愿测试次数是Lua中的100倍。

相关参考资料:

  1. Roberto Ierusalimschy. Lua Performance Tips. http://www.lua.org/gems/sample.pdf
  2. Lua Performance: http://springrts.com/wiki/Lua_Performance

目录

变量局部化

Lua变量区分为全局变量和局部变量。Lua为每一个函数分配了一套多达250个的寄存器,并用这些寄存器存储局部变量,这使得Lua中局部变量的访问速度很快。
相反的是,对于全局变量,Lua需要将全局变量读出存入当前函数的寄存器中,然后完成计算之后再存回全局变量表中。
这样,一个类似a = a + b这样的简单计算,若ab为局部变量,则编译之后只生成一条Lua指令,而若为全局变量,则生成4条Lua指令。Lua Performance Tips中提到,将全局变量变为局部变量,然后在使用进行访问,速度可以提升约30%,编写如下代码进行实验:

代码和结果

function nonlocal()
    local x = 0
    for i = 1, 100 do
        x = x + math.sin(i)
    end
    return x
end

local sin = math.sin
function localized()
    local x = 0
    for i = 1, 100 do
        x = x + sin(i)
    end
    return x
end

--[[------------------------
In Lua (1x)
ID  Case name   t1      t2      t3      t4      t5      avg.    %
1   Non-local   1.66    1.65    1.65    1.66    1.66    1.656   100%
2   Localized   1.25    1.25    1.25    1.25    1.25    1.25    75.48%
In LuaJIT (2x)
ID  Case name   t1      t2      t3      t4      t5      avg.    %
1   Non-local   1.3     1.31    1.3     1.3     1.3     1.302   100%
2   Localized   1.3     1.3     1.3     1.29    1.3     1.298   99.69%
--]]------------------------

结论
在普通Lua的解释下,行为和//Lua Performance Tips//中所述大致一致,调用全局变量中的函数大约会慢30%。
而在LuaJIT的解释下,两者差异不明显。
不是一定需要将全局变量中的变量转变为局部变量。

多重条件判断

在Lua中,唯一的数据结构table是哈希表,创建、销毁和迭代都需要创建很多资源。
本例对比当判断较多条件时,使用连续逻辑表达式和使用table的性能。

代码和结果

function use_or()
    local x = 0
    for _, v in pairs(states) do
        if "alabama" == v or
            "california" == v or
            "missouri" == v or
            "virginia" == v or
            "wisconsin" == v then
            x = x + 1
        end
    end
    return x
end

function use_table()
    local x, vals = 0, {"alabama", "california", "missouri", "virginia", "wisconsin"}
    for _, v in pairs(states) do
        for _, s in pairs(vals) do
            if s == v then x = x + 1 end
        end
    end
    return x
end

--[[------------------------
In Lua (1x)
ID  Case name   t1      t2      t3      t4      t5      avg.    %
1   Use OR      0.39    0.38    0.39    0.39    0.38    0.386   100%
2   Use table   1.85    1.84    1.84    1.85    1.84    1.844   477.72%
In LuaJIT (10x)
ID  Case name   t1      t2      t3      t4      t5      avg.    %
1   Use OR      0.6     0.6     0.59    0.59    0.6     0.596   100%
2   Use table   2.82    2.85    2.86    2.86    2.85    2.848   477.85%
--]]------------------------

结论
当判断较多逻辑条件时,应当使用简单的逻辑运算,而table创建、销毁和迭代的开销较大,应避免使用。
尽管使用循环的方法,程序可能具有较好的可读性,但是性能会严重下降。
使用逻辑表达式判断,采取较好的写法也可以获得可读性。

函数调用

函数是我们对功能进行封装的基本方法,但是函数的调用也是具有一定的开销,本例反映了函数调用的时间开销问题。
其中,直接计算部分在测试函数中内嵌代码进行阶乘计算,而函数调用部分则使用另外封装的阶乘计算函数。

代码和结果

function direct_compute()
    local x = 0
    for i = 1, 30 do
        local r = 1
        for j = 1, i do
            r = r * j
        end
        x = x + r
    end
    return x
end

function fact(x)
    local result = 1
    for i = 1, x do
        result = result * i
    end
    return result
end

function call_function()
    local x = 0
    for i = 1, 30 do
        x = x + fact(i)
    end
    return x
end

--[[------------------------
In Lua (1x)
ID  Case name         t1      t2      t3      t4      t5      avg.     %
1   Direct compute    1.17    1.17    1.17    1.17    1.18    1.172    100%
2   Call function     1.5     1.5     1.51    1.51    1.5     1.504    128.33%
In LuaJIT (10x)
ID  Case name         t1      t2      t3      t4      t5      avg.     %
1   Direct compute    1.08    1.08    1.08    1.08    1.07    1.078    100%
2   Call function     1.32    1.32    1.33    1.32    1.32    1.322    122.63%
--]]------------------------

结论
使用函数封装之后,消耗的时间Lua中增加28%,LuaJIT中增加22%。如果是会反复调用的功能,如无必要,如代码复用等问题,则应当尽可能避免多次调用函数。

尾递归

在一般的编程语言中,函数递归会占用栈空间,层次很深的递归容易导致栈溢出。
在一些编程语言,尤其是函数式编程语言中,对一种特殊的递归方法——尾递归进行了优化。
Lua中也是如此,使得尾递归的函数在递归开始时会释放当前函数的调用栈空间,从而保证多级递归也不会额外占用栈空间。
本例中展示尾递归的性能,使用普通递归、普通循环和尾递归三种方法编写计算阶乘的程序,并以普通递归方法作的数据为对比的基准。

代码和结果

-- 普通递归
function fact_recurse(x)
    if x <= 1 then return 1 end
    return x * fact_recurse(x - 1)
end

-- 普通循环
function fact_normal(x)
    local result = 1
    for i = 2, x do
        result = result * i
    end
    return result
end

-- 尾递归
function fact_tail(x, r)
    local result = r or 1
    if x <= 1 then return result end
    return fact_tail(x - 1, result * x)
end

--[[------------------------
In Lua (1x)
ID  Case name         t1      t2      t3      t4      t5      avg.     %
1   Normal recurse    1.88    1.88    1.89    1.88    1.88    1.882    100%
2   Normal loop       0.5     0.49    0.5     0.5     0.49    0.496    26.35%
3   Tail recurse      1.99    2       1.99    1.99    1.99    1.992    105.84%
In LuaJIT (20x)
ID  Case name         t1      t2      t3      t4      t5      avg.     %
1   Normal recurse    3.02    3       3       3       2.99    3.002    100%
2   Normal loop       0.98    0.97    0.97    0.97    0.98    0.974    32.45%
3   Tail recurse      1.06    1.07    1.07    1.07    1.07    1.068    35.58%
--]]------------------------

结论
虽然Lua中强调了对尾递归的优化,但是实际执行结果表明,Lua中的尾递归只是对栈空间的分配进行了优化,速度并没有比普通递归有提升。
而在LuaJIT中,使用尾递归编写的函数具有与普通循环相接近的性能。
不过尾递归函数不太容易编写,在实际使用过程中可以使用。

Table

table追加

Lua的标准库函数中,并不是所有函数都实现得很好,尤其是table数据结构的实现性能较差,table.insert函数就是一个性能较低的函数。

代码和结果

function table_insert()
    local t = {}
    for i = 1, 1000, 2 do
        table.insert(t, i)
    end
    return t
end

function table_insertL()
    local t, insert = {}, table.insert
    for i = 1, 1000, 2 do
        insert(t, i)
    end
    return t
end

function use_counter()
    local t, c = {}, 1
    for i = 1, 1000, 2 do
        t[c], c = i, c + 1
    end
    return t
end

function use_length()
    local t = {}
    for i = 1, 1000, 2 do
        t[#t + 1] = i
    end
end

--[[------------------------
In Lua (1x)
ID  Case name       t1      t2      t3      t4      t5      avg.    %
1   table.insert G  2.72    2.73    2.72    2.73    2.72    2.724   100%
2   table.insert L  2.24    2.24    2.24    2.24    2.24    2.24    82.23%
3   use counter     1.13    1.13    1.14    1.12    1.13    1.13    41.48%
4   use length      1.43    1.44    1.43    1.43    1.43    1.432   52.57%
In LuaJIT (5x)
ID  Case name       t1      t2      t3      t4      t5      avg.    %
1   table.insert G  3.69    3.69    3.68    3.68    3.69    3.686   100%
2   table.insert L  3.75    3.75    3.75    3.75    3.74    3.748   101.68%
3   use counter     0.86    0.85    0.85    0.86    0.85    0.854   23.17%
4   use length      3.71    3.71    3.71    3.71    3.71    3.71    100.65%
--]]------------------------

结论

当需要生成一个数组,并往数组的尾部添加数据时,应当尽可能的使用计数器,如果没办法使用计数器,也应当使用#运算符先求出数组的长度,然后使用计数器插入数组。

转:https://github.com/flily/lua-performance/blob/master/Guide.zh.md

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,517评论 0 38
  • 指令集 lua_capture_error_log lua_use_default_type lua_malloc...
    吃瓜的东阅读 11,864评论 0 2
  • 第一篇 语言 第0章 序言 Lua仅让你用少量的代码解决关键问题。 Lua所提供的机制是C不擅长的:高级语言,动态...
    testfor阅读 2,562评论 1 7
  • 函数有两种用途: 完成指定任务,此时函数作为调用语句使用。 计算并返回值,此时函数作为赋值语句的表达式使用。 调用...
    JunChow520阅读 3,622评论 0 3
  • 夏天到了,我们很多人都喜欢在车里面放各种各样的空气清新剂,我呢也从网上花了100多块钱买了一盒境界1号的空气清新剂...
    文致斌阅读 89评论 0 0