为什么这个Lua优化技巧会提高性能?

7
我正在查看一篇描述提高Lua脚本代码性能的各种技巧的文档,我很震惊这样的技巧是必需的。(虽然我引用了Lua,但我在Javascript中看到了类似的技巧。)
为什么需要这种优化呢?

For instance, the code

for i = 1, 1000000 do 
   local x = math.sin(i) 
end

runs 30% slower than this one:

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 不应该被优化。

2
Lua没有进行分析的能力吗?那像http://luaprofiler.luaforge.net/这样的工具呢? - Zack The Human
1
任何语言在其首选风格和性能之间都有权衡。Lua也不例外。 - RBerteig
@Zack 人类,不,我的意思是 Lua 没有被分析的能力。我无法访问编译器、运行时或使用的主进程。我只能访问写入或包含 Lua 代码的文件。 - Ian Boyd
6个回答

35
为什么这样做有帮助?编译器本来就应该做这个工作。为什么程序员要做编译器的工作呢?
Lua是一种动态语言。编译器在静态语言中可以进行很多推理,例如将常量表达式从循环中提取出来。在动态语言中,情况有些不同。
Lua的主要(也是唯一的)数据结构是表。math只是一个表,即使它在这里被用作命名空间。没有人可以阻止你在循环中的某个地方修改sin函数(即使这是一件不明智的事情),编译器在编译代码时无法知道这一点。因此,编译器会按照您的指示,在每次循环迭代中查找math表中的sin函数并调用它。
现在,如果您知道自己不会修改math.sin(即您将调用相同的函数),则可以在循环外部保存它到一个局部变量中。由于没有表查找,生成的代码更快。
对于LuaJIT,情况有些不同 - 它使用跟踪和一些高级技巧来查看您的代码在运行时正在做什么,因此它实际上可以通过移动表达式到循环外部等优化来优化循环,除了将其编译为机器代码外,使其变得非常快。
关于“将变量重新声明为本地变量”- 在定义模块时,您经常希望使用原始函数。当访问pairsmax或任何使用其全局变量的内容时,没有人能保证每次调用都是相同的函数。例如stdlib重新定义了许多全局函数。
通过创建与全局变量同名的局部变量,您实际上将函数存储到局部变量中,并且由于局部变量(词法作用域,意味着它们在当前作用域和任何嵌套作用域中可见)优先于全局变量,因此您确保始终调用相同的函数。如果有人稍后修改全局变量,则不会影响您的模块。更不用说这也更快,因为全局变量在全局表(_G)中查找。 更新:我刚刚阅读了Lua Performance Tips,作者之一Roberto Ierusalimschy编写的,它基本上解释了您需要了解的有关Lua,性能和优化的所有内容。在我看来,最重要的规则是:

规则#1:不要这样做。

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


它仍然没有解释为什么编译器无法弄清楚 - 但人们似乎对此非常有感觉。所以我想我必须接受它。 - Ian Boyd
11
编译器无法理解它,因为代码并不一直停留在Lua中 - 你可以从Lua调用已注册的C函数,这些函数又可以修改Lua环境。这些函数通常来自于模块(共享库)。难道你真的希望Lua编译器(总共只有约200Kb)去反编译这些库以便“理解它们”吗? - Michal Kottman

11

为什么默认不这样做,我不知道。然而,之所以更快,是因为本地变量被写入寄存器中,而全局变量意味着在表(_G)中查找,这已知会稍微慢一些。

至于可见性(例如格式函数):本地变量会遮蔽全局变量。因此,如果您声明一个与全局同名的本地函数,在其范围内将使用本地函数。如果您想要使用全局函数,则应使用_G.function。

如果您真正需要快速的Lua,可以尝试LuaJIT


1
我想这就是我的问题:为什么编译器不将它写入寄存器?如果总是更快,那么编译器应该查找全局变量一次,然后将其写入寄存器。这个函数不可能在我的函数运行时改变。 - Ian Boyd
2
@Ian Boyd,在Lua中,math.sin 函数的含义可以在您的函数运行时发生变化。在某些情况下,这种能力非常有价值。对于一些众所周知的库函数,这种能力可能并不那么有用,但它仍然是可能的,因此编译器必须尊重您实际编写的代码。 - RBerteig
math.sin 进行的一个可能有价值的运行时更改是通过 monkey-patch 添加一个 memoization 包装器。这仍然不是在循环期间执行的操作,但它仍然是发生在运行时的名为 math.sin 的全局变量的更改。 - RBerteig
@RBerteig 但是在我的循环或函数运行时,math.sin的值不可能改变,对吧?我想唯一可能改变它的方式是如果i改变了它 - 但是编译器知道我改变了它,因为它是编译器。另一个可能性是如果另一个线程决定修改全局系统的状态而没有任何保护措施 - 这将导致未定义的行为 - 因此Lua可以自由地执行任何内部优化,因为它不必做出任何承诺。 - Ian Boyd
2
@Ian Boyd,存储在名为math.sin的全局变量中的函数在调用时可能会更改该变量本身。Lua自带的标准版本不会这样做,但它可能会。编译器必须假定这是允许的(并且很少情况下甚至有用),这是一个需要适应的棘手问题。在Lua中需要习惯的一件棘手的事情是函数确实是存储在变量中的值。 - RBerteig

9

我在调试Lua环境时经常看到这种情况:人们会将变量重新声明为本地变量:

默认情况下这么做是错误的。

当一个函数被反复使用时,例如在你的示例循环内部,使用本地引用而不是表访问可能有用:

local sin = math.sin 
for i = 1, 1000000 do
  local x = sin(i) 
end

然而,在循环外部添加表格访问的开销完全可以忽略不计。

这里发生了什么,人们必须要做编译器的工作吗?

因为你上面提供的两个代码示例的含义并不完全相同。

这个函数不可能在我的函数运行时更改。

Lua是一种非常动态的语言,你不能像C等其他限制性更强的语言那样做出相同的假设。该函数可以在您的循环运行时更改。鉴于该语言的动态性,编译器无法假设该函数不会更改。或者至少不会在没有对您的代码及其影响进行复杂分析的情况下假设。

关键在于,即使您的两个代码片段看起来是等效的,在Lua中它们并不相同。在第一个代码片段中,您明确告诉它“在每次迭代中获取数学表中的sin函数”。在第二个代码片段中,您使用对同一函数的单个引用。

考虑以下内容:

-- The first 500000 will be sines, the rest will be cosines
for i = 1, 1000000 do 
   local x = math.sin(i)
   if i==500000 then math.sin = math.cos end 
end

-- All will be sines, even if math.sin is changed
local sin = math.sin
for i = 1, 1000000 do 
   local x = sin(i)
   if i==500000 then math.sin = math.cos end 
end

3
将函数存储在本地变量中可以避免在循环的每次迭代中查找函数键的表索引,数学函数显而易见,因为它需要在Math表中查找哈希值,其他函数则是在_G(全局表)中进行索引,自5.2版本起改为在_ENV(环境表)中进行索引。
此外,可以使用其调试钩子API来对Lua进行性能分析,或者使用可用的Lua调试器。

你是指表索引。math,_G等只是普通的表格。 - jpjacobs

1

这不仅是Lua的一个错误/特性,许多语言包括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 表及其成员/值“本地化”到“文件范围”。


1

我的假设是,在优化版本中,因为函数的引用存储在本地变量中,所以不必在每次for循环迭代(查找math.sin)时执行树遍历。

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

然而,我可能完全错了 ;)

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接