为什么这个Lua优化黑客能够提高性能?

时间:2011-01-10 05:01:35

标签: optimization scripting lua premature-optimization

我正在查看document that describes various techniques to improve performance of 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. ,则不应进行优化
  3. Lua无法被描述
  4. Lua不应该优化

6 个答案:

答案 0 :(得分:34)

  

为什么这会有所帮助?无论如何,这是编译器的工作。为什么程序员必须完成编译工作?

Lua是一种动态语言。编译器可以在静态语言中做很多推理,比如将循环中的常量表达式拉出来。在动态语言中,情况有点不同。

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

现在,如果你知道你不打算修改math.sin(即你要调用相同的函数),你可以将它保存在循环外的局部变量中。由于没有表查找,因此生成的代码更快。

LuaJIT的情况有点不同 - 它使用跟踪和一些高级魔法来查看你的代码在运行时中做什么,所以它实际上可以通过将表达式移到外部来优化循环循环和其他优化,除了实际将其编译为机器代码,使其快速疯狂。

关于'重新声明变量为本地' - 多次定义模块时,您希望使用原始函数。当使用全局变量访问pairsmax或任何内容时,没有人可以向您保证每次调用它都是相同的功能。例如,stdlib重新定义了许多全局函数。

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

更新:我刚刚阅读了Lua作者之一Roberto Ierusalimschy撰写的Lua Performance Tips,它几​​乎解释了您需要了解的有关Lua,性能和优化的所有内容。 IMO最重要的规则是:

  

规则#1 :不要这样做。

     

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

答案 1 :(得分:11)

我不知道默认情况下没有这样做的原因。然而,为什么它更快是因为本地人被写入寄存器,而全局意味着在表(_G)中查找它,这已知有点慢。

关于可见性(与格式函数一样):局部模糊了全局。因此,如果声明一个与全局同名的本地函数,则只要它在范围内,就会使用本地函数。如果您想要使用全局函数,请使用_G.function。

如果你真的想 Lua,你可以尝试LuaJIT

答案 2 :(得分: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 :(得分:3)

在局部变量中存储函数会删除表索引以在循环的每次迭代中查找函数键,数学表明是明显的,因为它需要在Math表中查找散列,其他不是,它们是索引到_G(全局表),现在是_ENV(环境表),从5.2开始。

此外,应该能够使用其调试挂钩API或使用位于其周围的lua调试器来配置lua。

答案 4 :(得分:1)

我的假设是在优化版本中,因为对函数的引用存储在局部变量中,所以不必在for循环的每次迭代中都进行树遍历(用于查找{{1} })。

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

然后再次,我可能会离开基地;)

编辑:我还假设Lua编译器是愚蠢的(无论如何,这是我对编译器的一般假设;)

答案 5 :(得分: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表及其成员/值'本地化'到'文件范围'。