前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用工具优化Lua的table访问

使用工具优化Lua的table访问

原创
作者头像
esrrhs
发布2023-03-06 17:02:36
4650
发布2023-03-06 17:02:36
举报
文章被收录于专栏:我的开源

背景

写Lua代码似乎不需要考虑性能,毕竟都用Lua了,如果考虑性能直接用C++不就好了。但是勤俭节约是中华民族传统美德,能省点cpu是一点。特别是在Lua使用越来越多的时候。

示例

考虑如下写法:

代码语言:lua
复制
local a = {}
a.b = {}
a.b.data = "a"
a.b.c = {}
a.b.c.data = "b"
a.b.c.d = {}
a.b.c.d.data = "c"
a.b.c.d.e = {}

上面的代码,每次访问a.b都会触发一次table的访问,这样会影响性能。比较好的写法是使用一个local变量接一下,如下所示:

代码语言:lua
复制
local a = {}
a.b = {}
local a_b = a.b
a_b.data = "a"
a_b.c = {}
local a_b_c = a_b.c
a_b_c.data = "b"
a_b_c.d = {}
local a_b_c_d = a_b_c.d
a_b_c_d.data = "c"
a_b_c_d.e = {}

优化后的代码,执行速度是之前的2倍(测试代码是一个a-z的连续table访问)

但是显而易见,代码可读性差了一点点,不是那种看一眼就知道做了什么事情的代码,而且写起来也不是太舒服,远没有一顿复制粘贴来的爽。

方案

那么我们可以用工具来解决这个问题,用工具分析出这样的代码,然后进行替换。

这个工具听起来好像很简单,实际上也雀食不复杂,但是还是有一些细节点需要注意。

替换规则

首先要明确替换的规则,从前面的例子可以看到规律,当对代码a.b反复使用的时候,就应该替换了。比如:

代码语言:lua
复制
a.b.c = 1
a.b.d = 2

有人可能会问,反复使用a的不替换吗?像这样:

代码语言:lua
复制
a.b = 1
a.c = 2
a.d = 3

答:这种也不用替换吧,a本身就是一个变量了。

那么怎么替换a.b,是分析读和写,来替换吗?比如在对a.b的写之前,我都可以用一个变量来代替a.b的读,这样就可以加速了。

但是这里有个问题,Lua是一门特别灵活的语言,你甚至不知道a.b是不是一个table。又或者运行中变成了一个另一个类型。更不提经过了函数的一圈调用,长什么样连代码作者可能都不知道。要静态分析出读和写,是很难的。

所以我们做一个假设推断,当我对一个a.b赋值构造的table后,就不会再更改a.b为其他table或者其他类型,这样我就可以把后续所有的a.b替换成局部变量,就像上面的示例代码那样。

违反这个假设的是这样的代码:

代码语言:lua
复制
a.b = {}
a.b.c = "1"
a.b.d = "2"
a.b = {}

如果替换为:

代码语言:lua
复制
a.b = {}
local a_b = a.b
a_b.c = "1"
a_b.d = "2"
a_b = {}

优化后的a.b并没有变成空table,与优化前不等价了。所以这样的代码是不适用工具优化的,当然值得庆幸的是这样的代码并不多。

有人又会问了,我如果用c接一下a.b,怎么办?比如:

代码语言:lua
复制
a.b = {}
a.b.c = "1"
a.b.d = "2"
c = a.b
c.e = "3"

显然,这样的替换之后,也是OK的

代码语言:lua
复制
a.b = {}
local a_b = a.b
a_b.c = "1"
a_b.d = "2"
c = a_b
c.e = "3"

因为table的赋值,只是引用,传递的是指针,所以大家的修改都是等效的。

替换的小细节

有一种情况是无需替换的,那就是后续对a.b的使用只有1次的情况(0次当然更不会替换)比如这样的代码:

代码语言:lua
复制
a.b = {}
a.b.c = 1

这里,就算替换成了

代码语言:lua
复制
a.b = {}
local a_b = a.b
a_b.c = 1

发现没有,总的table访问次数是一样的,所以这种就不用白费力气了。

正则匹配 or 语法解析

既然搞清楚怎么替换,那还不简单,我直接正则匹配行不行?应该可行,但是还是分析语法更容易些,比如要替换这样的代码:

代码语言:txt
复制
a.b = {
    k1 = 1,
    k2 = 2,
    k3 = {3},
}
a.b.c = 1
a.b.d = 2

要找到待替换项a.b就得费挺大劲。还不如分析下语法更快,有很多现成的分析Lua语法的库可供使用。

作用域

另一个要考虑的是作用域问题,每一个替换只能影响他所在的作用域,例如下面的代码:

代码语言:lua
复制
if c then
    local a.b = {}
    a.b.c = 1
    a.b.d = 2
else
    a.b.e = 3
end

如果粗暴的把a.b替换,那么就变成:

代码语言:lua
复制
if c then
    local a.b = {}
    local a_b = a.b
    a_b.c = 1
    a_b.d = 2
else
    a_b.e = 3
end

a_b在下面的block里,是未定义的,这就不对了,所以替换只能发生在他所在的作用域中。

另外前面讲到的判断是否替换(对a.b的使用次数大于1)也是一样的要考虑作用域问题,例如:

代码语言:lua
复制
if c then
    local a.b = {}
    a.b.c = 1
else
    a.b.d = 2
    a.b.e = 3
end

虽然这里a.b使用了很多次,但是在then的block里,只有1次使用,因为不会发送替换操作。

local代码插入

前面讲完了替换,该插入关键的local a_b = a.b代码了。

这一行代码怎么插入呢?有的人会说,很简单,在a.b = {}的下一行插入不就好了?

那么考虑下这种代码:

代码语言:lua
复制
a.b = {
    k1 = 1,
    k2 = 2,
}
a.b.c = 1
a.b.d = 2

因此这一句local必须插在a.b整个table construct之后才行。也就是

代码语言:lua
复制
a.b = {
    k1 = 1,
    k2 = 2,
}
local a_b = a.b
a_b.c = 1
a_b.d = 2

所以需要分析整个table所占的行才行,不幸的是,很多库,分析出来的语法树,对于末尾的}是不感知的,毕竟那个东西并没有实际意义。

因此问题来了,如何拿到这一大坨的最后一句代码?然后插入我们的local?

很简单,我们取他下一句代码的行号,只需要保证在那一行之前插入local就行了。也就是获取a.b.c = 1这一句的初始行号。

又有人会问了,如果下一行没有代码呢?也就是a.b = {}就是它所在block的最后一句代码,那相当于是这样的:

代码语言:lua
复制
if c then
    a.b = {}
end

这种a.b显然也不会需要被替换。

另一个比较奇葩的点,是有的Lua会这么写:

代码语言:lua
复制
a.b = {
    k1 = 1,
    k2 = 2,
};
a.b.c = 1
a.b.d = 2

这里大括号后,多写了分号,执行没什么问题,但是有的库会在这里生成一个空的block,需要跳过下,不然上面的插入local的逻辑也会有问题。

结尾

搞了这么多细节,最后实际项目中的优化效果如何呢?

首先,这种替换的场景本身就不多,大部分的Lua代码写的还是比较优秀的。

其次,优化的table访问占整个大盘的百分比也是很小的。

但是你说完全没有效果吗,也不对。所以,就像导语说的,这是一个聊胜于无的优化。

当然,对于一些特殊场景,比如本身没什么计算逻辑,但是全是table get,那么优化会有明显的效果了。在实际应用中,有约10%的提升。

最后,基于前面的假设,工具 的优化并不是万能的,只是作为一个辅助,对于优化后的代码,还需要其他手段来验证是否完全等价。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 示例
  • 方案
    • 替换规则
      • 替换的小细节
        • 正则匹配 or 语法解析
          • 作用域
            • local代码插入
            • 结尾
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档