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版本做过优化了)。
1556 0
|
人工智能 算法 程序员
|
6月前
|
存储 NoSQL Redis
Redis的Lua脚本有什么作用?
Redis Lua脚本用于减少网络开销、实现原子操作及扩展指令集。它能合并操作降低网络延迟,保证原子性,替代不支持回滚的事务。通过脚本,代码复用率提高,且可自定义指令,如实现分布式锁,增强Redis功能和灵活性。
249 1
|
1月前
|
缓存 分布式计算 NoSQL
大数据-43 Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
大数据-43 Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
29 2
|
5月前
|
消息中间件 NoSQL Java
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
227 0
|
2月前
|
存储 JSON Ubuntu
如何使用 Lua 脚本进行更复杂的网络请求,比如 POST 请求?
如何使用 Lua 脚本进行更复杂的网络请求,比如 POST 请求?
|
3月前
|
存储 NoSQL Redis
Tair的发展问题之在Redis集群模式下,Lua脚本操作key面临什么问题,如何解决
Tair的发展问题之在Redis集群模式下,Lua脚本操作key面临什么问题,如何解决
|
6月前
|
缓存 NoSQL Java
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
227 0
|
5月前
|
JSON 监控 数据格式
使用Lua代码扩展上网行为管理软件的脚本功能
本文介绍了如何使用Lua脚本增强上网行为管理,包括过滤URL、记录用户访问日志、控制带宽和自动提交监控数据到网站。Lua是一种轻量级语言,适合编写扩展脚本。文中提供多个示例代码,如URL过滤器、用户活动日志记录器和带宽控制器,帮助用户根据需求定制网络管理功能。通过这些示例,用户可以快速掌握Lua在上网行为管理中的应用。
173 4
|
5月前
|
NoSQL API Redis
使用Redis Lua脚本实现高级限流策略
使用Redis Lua脚本实现高级限流策略
199 0