为什么这个 Lua 优化小技巧会提高性能呢?
我正在查看一份描述改善Lua脚本代码性能的各种技术的文档,我惊讶于为什么需要这样的技巧。(虽然我引用了Lua,但我在Javascript中也看到了类似的技巧)。
为什么需要这种优化:
例如,下面的代码
for i = 1, 1000000 do local x = math.sin(i) end
运行速度比这个慢30%:
local sin = math.sin for i = 1, 1000000 do local x = sin(i) end
它们正在本地重新声明sin
函数。
为什么这有用?这本来就是编译器的工作。为什么程序员要做编译器的工作?
我在Javascript中看过类似的事情;因此显然必须有一个非常好的理由,解释器编译器没有执行其工作。是什么原因?
我在我正在研究的Lua环境中反复看到这种情况;人们重新声明变量为本地变量:
local strfind = strfind
local strlen = strlen
local gsub = gsub
local pairs = pairs
local ipairs = ipairs
local type = type
local tinsert = tinsert
local tremove = tremove
local unpack = unpack
local max = max
local min = min
local floor = floor
local ceil = ceil
local loadstring = loadstring
local tostring = tostring
local setmetatable = setmetatable
local getmetatable = getmetatable
local format = format
local sin = math.sin
这里发生了什么,人们必须执行编译器的工作?编译器无法找到format
吗?这为什么是程序员必须处理的问题?为什么在1993年就不能解决这个问题?
我似乎还碰到了一个逻辑悖论:
- 不应在没有分析数据的情况下进行优化
- Lua无法进行分析数据
- 不应优化Lua
原文链接 https://stackoverflow.com/questions/4643814
在本地变量中存储函数可以消除表索引以每次循环查找函数键,数学函数是显而易见的,因为它需要在Math表中查找哈希,其他函数则不是,它们被索引到_G
(全局表),从5.2版本开始变成了_ENV
(环境表)。
此外,可以使用其调试钩子API或使用已有的Lua调试器对Lua进行分析。
原文 The reason why it's not done by default, I don't know. Why it's faster however is because locals get written to a register, while a global means looking it up in a table (_G), which is known to be somewhat slower.
As for the visibility (like with the format function): A local obscures the global. So if you declare a local function with the same name as a global, the local will be used instead as long as it is in scope. If you would want to use the global function instead, use _G.function.
If you really want fast Lua, you could try LuaJIT
翻译
为什么不默认进行这个操作,我不知道。然而之所以更快的原因是因为局部变量被写入寄存器,而全局变量意味着在一张表(\_G
)找它,这被认为有点慢。
至于可见性(例如对于格式化函数),局部变量将掩盖全局变量。如果您声明了一个与全局变量同名的局部函数,只要在其范围内,就会首先使用局部函数。如果您想使用全局函数,则使用\_G.function
。
如果你真的想要速度_快_的Lua,可以尝试 LuaJIT。
为什么这会有帮助?这应该是编译器的工作。为什么程序员要做编译器的工作呢?
Lua是一种动态语言。编译器可以在静态语言中进行很多推理,例如从循环中提取常量表达式。在动态语言中,情况有所不同。
Lua的主要数据结构是表。即使在这里作为命名空间使用,math
也只是一个表。没有人可以阻止您在循环中的某个地方修改math.sin
函数(即使这是不明智的事情),编译器在编译代码时也不知道这一点。因此,编译器完全按照您的指令执行操作:在循环的每次迭代中,在math
表中查找sin
函数并调用它。
现在,如果您知道您不会修改math.sin
(即您将调用相同的函数),可以将其保存在循环外的局部变量中。因为没有表查找,所以结果是更快的代码。
在LuaJIT中情况有所不同-它使用跟踪和一些高级技巧来查看您的代码在运行时正在执行,因此它实际上可以通过将表达式移动到循环之外来优化循环,并进行其他优化,除了实际编译为机器代码,使其变得非常快。
关于“将变量重新声明为本地变量”的情况-在定义模块时,很多时候您想使用原始函数。当使用其全局变量访问pairs
,max
或任何东西时,没有人能保证每次调用都是相同的函数。例如stdlib重新定义了很多全局函数。
通过创建与全局变量相同名称的局部变量,您实际上将函数存储到局部变量中,并且因为局部变量(在词法范围内,意味着它们在当前范围和任何嵌套的范围中都可见)在全局变量之前领先,因此可以确保始终调用相同的函数。如果有人稍后修改全局变量,则不会影响您的模块。更不用说这也更快,因为全局变量在全局表(_G
)中查找。
更新:我刚刚阅读了Lua作者之一Roberto Ierusalimschy的《Lua Performance Tips》,这本书几乎解释了您需要了解的有关Lua,性能和优化的所有内容。我认为最重要的规则是:
规则#1:不要这么做。
规则#2:暂时不要这么做。(仅限专家)
我在玩弄的 Lua 环境中反复看到人们将变量重新声明为局部变量:
默认情况下这样做是明显错误的。
当一个函数重复使用时,类似于你的示例循环内部,使用局部变量引用而不是表访问可能是有用的:
local sin = math.sin
for i = 1, 1000000 do
local x = sin(i)
end
然而,在循环外,添加表访问的开销完全可以忽略不计。
这里发生了什么,人们必须要做编译器的工作?
因为你做的两个代码样本并不完全意味着相同的事情。
不像这个函数可以在我的函数运行时更改。
Lua 是一门非常动态的语言,你无法像某些限制更多的语言(例如 C)一样作出相同的假设。这个函数可能会在循环运行时更改。鉴于语言的动态性,编译器不能假设该函数不会更改。或者至少不是在对你的代码及其影响进行复杂分析之前。
但是技巧在于,即使你的两段代码看起来是等价的,在 Lua 中它们并不是等价的。第一个示例告诉它在每次迭代中“获取 math 表内的 sin 函数”。在第二个示例中,你使用了对相同函数的单个引用。
考虑以下内容:
-- 前 500000 个将是正弦函数,其余将是余弦函数
for i = 1, 1000000 do
local x = math.sin(i)
if i==500000 then math.sin = math.cos end
end
-- 所有都将是正弦函数,即使 math.sin 被更改了
local sin = math.sin
for i = 1, 1000000 do
local x = sin(i)
if i==500000 then math.sin = math.cos end
end
这不仅是 Lua
的一个 bug/feature,许多编程语言包括 Java
和 C
都会呈现出如果访问本地变量而不是作用域之外的变量(如来自类或数组)则能提高速度。
例如,在 C++
中,访问本地成员比访问某些类的变量成员更快。
如下代码将更快地计数到 10,000:
for (int i = 0; i < 10000; i++) {
}
而非:
for (myClass.i = 0; myClass.i < 10000; myClass.i++) {
}
Lua
之所以将全局变量保存在表格内部,是因为它允许程序员通过更改 _G
引用的表格来快速保存和更改全局环境。我同意这样做更直接的方式是将全局表 _G
视为一个特殊情况;将它们都重写为文件作用域的本地变量(或类似的方法),当然这并不妨碍我们自己这样做;或许可以编写一个函数 optGlobalEnv(...)
使用 unpack()
将 _G
表与其成员/值“本地化”到文件作用域。
- 如何在roblox studio中1:1导入真实世界的地形?
- 求解,lua_resume的第二次调用继续执行协程问题。
- 【上海普陀区】内向猫网络招募【Skynet游戏框架Lua后端程序员】
- SF爱好求教:如何用lua实现游戏内调用数据库函数实现账号密码注册?
- Lua实现网站后台开发
- LUA错误显式返回,社区常见的规约是怎么样的
- lua5.3下载库失败
- 请问如何实现文本框内容和某个网页搜索框内容连接,并把网页输出来的结果反馈到另外一个文本框上
- lua lanes多线程使用
- 一个kv数据库
- openresty 有没有比较轻量的 docker 镜像
- 想问一下,有大佬用过luacurl吗
- 在Lua执行过程中使用Load函数出现问题
- 为什么 neovim 里没有显示一些特殊字符?
- Lua比较两个表的值(不考虑键的顺序)
- 有个lua简单的项目,外包,有意者加微信 liuheng600456详谈,最好在成都
- 如何在 Visual Studio 2022 中运行 Lua 代码?
- addEventListener 返回 nil Lua
- Lua中获取用户配置主目录的跨平台方法
- 如何编写 Lua 模式将字符串(嵌套数组)转换为真正的数组?
我的假设是,在优化版本中,由于函数的引用存储在一个局部变量中,在
for
循环的每一次迭代中,不必执行一次树遍历(用于查找math.sin
)。我不确定函数名称所设置的局部引用,但我假设如果没有找到局部引用,则需要进行某种全局命名空间查找。
另一方面,我可能也有些偏离主题了 ;)
编辑:我还假设 Lua 编译器很笨(这对于我对编译器的一般假设来说,是一个共同的假设;)