为什么这个 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年就不能解决这个问题?


我似乎还碰到了一个逻辑悖论:

  1. 不应在没有分析数据的情况下进行优化
  2. Lua无法进行分析数据
  3. 不应优化Lua

原文链接 https://stackoverflow.com/questions/4643814

点赞
stackoverflow用户534476
stackoverflow用户534476

我的假设是,在优化版本中,由于函数的引用存储在一个局部变量中,在 for 循环的每一次迭代中,不必执行一次树遍历(用于查找 math.sin)。

我不确定函数名称所设置的局部引用,但我假设如果没有找到局部引用,则需要进行某种全局命名空间查找。

另一方面,我可能也有些偏离主题了 ;)

编辑:我还假设 Lua 编译器很笨(这对于我对编译器的一般假设来说,是一个共同的假设;)

2011-01-10 05:10:10
stackoverflow用户255049
stackoverflow用户255049

在本地变量中存储函数可以消除表索引以每次循环查找函数键,数学函数是显而易见的,因为它需要在Math表中查找哈希,其他函数则不是,它们被索引到_G(全局表),从5.2版本开始变成了_ENV(环境表)。

此外,可以使用其调试钩子API或使用已有的Lua调试器对Lua进行分析。

2011-01-10 05:18:21
stackoverflow用户513763
stackoverflow用户513763

原文 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

2011-01-10 09:24:23
stackoverflow用户221509
stackoverflow用户221509

为什么这会有帮助?这应该是编译器的工作。为什么程序员要做编译器的工作呢?

Lua是一种动态语言。编译器可以在静态语言中进行很多推理,例如从循环中提取常量表达式。在动态语言中,情况有所不同。

Lua的主要数据结构是表。即使在这里作为命名空间使用,math也只是一个表。没有人可以阻止您在循环中的某个地方修改math.sin函数(即使这是不明智的事情),编译器在编译代码时也不知道这一点。因此,编译器完全按照您的指令执行操作:在循环的每次迭代中,在math表中查找sin函数并调用它。

现在,如果您知道您不会修改math.sin(即您将调用相同的函数),可以将其保存在循环外的局部变量中。因为没有表查找,所以结果是更快的代码。

在LuaJIT中情况有所不同-它使用跟踪和一些高级技巧来查看您的代码在运行时正在执行,因此它实际上可以通过将表达式移动到循环之外来优化循环,并进行其他优化,除了实际编译为机器代码,使其变得非常快。

关于“将变量重新声明为本地变量”的情况-在定义模块时,很多时候您想使用原始函数。当使用其全局变量访问pairsmax或任何东西时,没有人能保证每次调用都是相同的函数。例如stdlib重新定义了很多全局函数。

通过创建与全局变量相同名称的局部变量,您实际上将函数存储到局部变量中,并且因为局部变量(在词法范围内,意味着它们在当前范围和任何嵌套的范围中都可见)在全局变量之前领先,因此可以确保始终调用相同的函数。如果有人稍后修改全局变量,则不会影响您的模块。更不用说这也更快,因为全局变量在全局表(_G)中查找。

更新:我刚刚阅读了Lua作者之一Roberto Ierusalimschy的《Lua Performance Tips》,这本书几乎解释了您需要了解的有关Lua,性能和优化的所有内容。我认为最重要的规则是:

规则#1:不要这么做。

规则#2:暂时不要这么做。(仅限专家)

2011-01-10 12:45:56
stackoverflow用户312586
stackoverflow用户312586

我在玩弄的 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
2011-01-10 16:04:39
stackoverflow用户1809263
stackoverflow用户1809263

这不仅是 Lua 的一个 bug/feature,许多编程语言包括 JavaC 都会呈现出如果访问本地变量而不是作用域之外的变量(如来自类或数组)则能提高速度。

例如,在 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 表与其成员/值“本地化”到文件作用域。

2012-11-08 12:28:15