7

我正在查看一个文档,该文档描述了提高 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 不应该被优化
4

6 回答 6

35

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

Lua 是一种动态语言。编译器可以在静态语言中进行大量推理,例如将常量表达式拉出循环。在动态语言中,情况有点不同。

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

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

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

关于“将变量重新声明为本地” - 很多时候在定义模块时,您希望使用原始函数。在访问或使用其全局变量的任何内容时pairsmax没有人可以向您保证每次调用都将是相同的函数。比如stdlib重新定义了很多全局函数。

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

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

规则#1:不要这样做。

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

于 2011-01-10T12:45:56.207 回答
11

默认情况下不做的原因,我不知道。然而,为什么它更快是因为局部变量被写入寄存器,而全局变量意味着在表 (_G) 中查找它,众所周知这有点慢。

至于可见性(如使用格式功能):局部遮蔽全局。因此,如果您声明一个与全局同名的局部函数,只要它在范围内,就会使用局部函数。如果您想改用全局函数,请使用 _G.function。

如果你真的想要快速Lua,你可以试试LuaJIT

于 2011-01-10T09:24:23.637 回答
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
于 2011-01-10T16:04:39.880 回答
3

将函数存储在局部变量中会删除表索引以在循环的每次迭代中查找函数键,数学是显而易见的,因为它需要在数学表中查找哈希,其他则不需要,它们被索引到_G(全局表),现在_ENV是 5.2 的(环境表)。

此外,应该能够使用其调试钩子 API 或使用周围的 lua 调试器来分析 lua。

于 2011-01-10T05:18:21.640 回答
1

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

于 2012-11-08T12:28:15.550 回答
1

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

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

再说一次,我可能离基地很远;)

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

于 2011-01-10T05:10:10.053 回答