Lua性能优化指南

简介: 本文主要取材于Lua Programming Gems一书的第二章Lua Performance Tips,总结了Lua性能优化的几个基本要点,并对每个点给出了具体的测试代码和时间性能对比数据。

说明

本文主要取材于Lua Programming Gems一书的第二章Lua Performance Tips, 原书试读章节可点击这里下载

测试代码的运行环境均为Raspberry Pi 3, Lua 5.1.5

性能优化的基本原则

  1. 能不优化则不优化
  2. 先量化再优化:高手和菜鸟之间的区别不在于高手对于需要优化的点直觉更准,而是高手更清楚自己的经验和感觉都是不可靠的,只能依靠测试数据来定位性能瓶颈。

多用局部变量

就表达式 a = a + b而言,当a, b都是局部变量时,Lua虚拟机只需要执行1条指令就能完成这个加法操作;而当a, b都是全局变量时,则需要执行4条指令才能完成。

先看一个很明显使用了全局变量的例子

-- global_a.lua
a = 1
b = 1
for i = 1, 1000000 do
    a = a + b
end
-- local_a.lua
local a = 1
local b = 1
for i = 1, 1000000 do
    a = a + b
end

对比两段代码的运行时间, 使用局部变量的版本耗时仅为全局变量的30%

$ time lua global_a.lua
real    0m0.287s

$ time lua local_a.lua
real    0m0.093s

再看一个不那么明显的例子,对比下面两段代码的运行时间

-- global_sin.lua
for i = 1, 1000000 do
    local x = math.sin(i)
end
-- local_sin.lua
local sin = math.sin
for i = 1, 1000000 do
    local x = sin(i)
end

使用局部变量的版本耗时约为全局变量的70%

$ time lua global_sin.lua 
real    0m0.663s

$ time lua local_sin.lua 
real    0m0.509s

少用动态代码

由于编译代码是一件很消耗CPU的事情,出于性能的考虑,应该尽量使用静态代码,少用动态代码。

对比下面两段代码的运行时间

-- dynamic_code.lua
local lim = 10000
local a = {}
for i = 1, lim do
    a[i] = loadstring(string.format("return %d", i))
end
print(a[10]())  --> 10
-- static_code.lua
local lim = 10000
local a = {}
local fk = function(k) return function() return k end end
for i = 1, lim do
    a[i] = fk(i)
end
print(a[10]())  --> 10

使用静态代码的版本耗时仅仅是动态代码的10%

$ time lua dynamic_code.lua 
10
real    0m0.219s

$ time lua static_code.lua 
10
real    0m0.024s

定义小表时指定初始元素

表是Lua中唯一的数据结构,它的实现分为两部分: 数组部分和散列部分。数组部分用于存放从1开始的连续整数key,散列部分用于存放和已有数组索引不连续的整数以及其他类型的key。例如, t = {100, 200, 300, x = 9.2, pi = 3.14} 的数据实现如下图: [1]

lua_table

为了尽可能节省内存,Lua虚拟机在定义一个空表时不会为表内元素预先分配空间。当插入新元素由于表的容量不足而需要扩容时,Lua虚拟机会将表的容量增长到不小于元素个数的2的幂。散列部分的扩容操作涉及下面两个动作:

  1. 为bucket数组分配一个更大的空间
  2. 重新散列表内元素

因此,当我们定义一个空表T并往里面插入3个字符串元素时,T的散列部分的bucket数组会经历先从0(空表)到1(2的0次幂),再从1到2(2的1次幂),最后从2到4(2的2次幂)一共三次的扩容过程。但是,如果在定义这个大小为3的表时指定了3个初始元素,Lua虚拟机就会一次性为这个表的bucket数组分配4的容量,并消除三次扩容中重新散列的的计算过程,从而实现更优的性能。

对比下面两段代码的运行时间

-- empty_table.lua
for i = 1, 1000000 do
    local t = {}
    t['e'] = 1; t['s'] = 2; t['m'] = 3
end
-- known_table.lua
for i = 1, 1000000 do
    local t = {e = 1, s = 2, m = 3}
end

指定了初始元素的版本耗时不到空表版本的50%

$ time lua empty_table.lua
real    0m4.290s

$ time lua known_table.lua
real    0m1.962s

以上对比了只使用了散列部分的表的两种初始化方式的性能,我们再来看只使用了数组部分的表的情况。数组部分的扩容操作和散列部分的bucket数组的扩容操作是一样的。只使用数组部分的表的扩容虽不涉及重新散列操作,但仍需要重新分配内存和复制已有元素,因此预分配的实现依然更为高效。

对比下面两段代码的运行时间

-- empty_array.lua
for i = 1, 1000000 do
    local t = {}
    t[1] = 'e'; t[2] = 's'; t[3] = 'm'
end
-- known_array.lua
for i = 1, 1000000 do
    local t = {'e', 's', 'm'}
end

指定了初始元素的版本耗时不到空表版本的50%

$ time lua empty_array.lua
real    0m0.870s

$ time lua known_array.lua
real    0m0.340s

使用table.concat函数实现多个字符串的拼接操作

Lua对于相同的字符串只保留一份拷贝,所有变量均是指向字符串的引用。这样设计的优点除了能节省内存,还能在常数时间内实现两个任意长度字符串的比较操作。但弱点在于进行两个字符串的拼接操作时必须先将前面那个字符串复制一个副本出来,再将后面那个字符串添加到该副本后面。因此,如果直接使用“..”运算符来实现字符串拼接,时间复杂度关于拼接字符串的个数不是线性的,而是成平方关系。为了解决这个问题,Lua引入了能使字符串拼接操作保持线性时间复杂度的标准库函数table.concat。

对比下面两段代码的运行时间

-- direct_append.lua
r = ""
s = "0123456789abcdefghijklmnopqrstuvwxyz"
for i = 1, 10000 do
    r = r .. s
end
-- table_concat.lua
s = "0123456789abcdefghijklmnopqrstuvwxyz"
t = {}
for i = 1, 10000 do
    t[i] = s
end
table.concat(t)

使用table.concat的版本耗时不到直接拼接的1%

$ time lua direct_append.lua
real    0m2.924s
$ time lua table_concat.lua
real    0m0.026s

[1] R. Ierusalimschy, L. H. Figueiredo, and W. Celes. The Implementation of Lua 5.0, pages 6-8, https://www.lua.org/doc/jucs05.pdf.

目录
相关文章
lua的性能优化
Roberto Ierusalimschy写过经典的Lua 性能提示的文章,链接地址>> 我通过实际的代码来验证,发现一个问题。当我使用 LuaStudio 运行时,发现结果反而与提示相反,甚是奇怪,而使用luac进行运行,与作者给予的提示相符,在某些地方性能可能有优化,比如读取35kb的文件时,时间还是比较快的(可能5.1版本做过优化了)。
1519 0
|
人工智能 算法 程序员
|
3月前
|
存储 NoSQL 关系型数据库
使用lua脚本操作redis
使用lua脚本操作redis
50 0
|
3月前
|
NoSQL Java Redis
Redis进阶-lua脚本
Redis进阶-lua脚本
59 0
|
1月前
|
缓存 NoSQL Java
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
62 0
|
2月前
|
算法 NoSQL Java
springboot整合redis及lua脚本实现接口限流
springboot整合redis及lua脚本实现接口限流
76 0
|
1天前
|
存储 NoSQL 调度
Redis Lua脚本:原子性的真相揭秘
【4月更文挑战第20天】
4 0
Redis Lua脚本:原子性的真相揭秘
|
20天前
|
NoSQL Java Redis
lua脚本做redis的锁
这段内容是关于使用Redis实现分布式锁的Java代码示例。`RedisLock`类包含`lock`和`unlock`方法,使用`StringRedisTemplate`和Lua脚本进行操作。代码展示了两种加锁方式:一种带有过期时间,另一种不带。还提到了在加锁和解锁过程中的异常处理,并提供了相关参考资料链接。
17 3
|
23天前
|
存储 NoSQL 数据处理
Redis Lua脚本:赋予Redis更强大的逻辑与功能
Redis Lua脚本:赋予Redis更强大的逻辑与功能
|
1月前
|
监控
通过Lua脚本实现禁止员工上班玩游戏的软件的自动化任务管理
使用Lua脚本,企业可以自动化管理员工行为,防止上班时间玩游戏。Lua是一种轻量级脚本语言,适合编写监控任务。示例脚本展示了如何检测工作时间内员工是否玩游戏,并在发现时执行相应操作,如关闭游戏或发送警告。此外,另一脚本演示了如何将监控数据通过HTTP POST自动提交到网站,以实现有效的行为管理。这种解决方案灵活且可定制,有助于提升工作效率。
120 1