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

Ian*_*oyd 7 scripting optimization lua premature-optimization

我正在查看描述各种提高Lua脚本代码性能的技术文档,我很震惊需要这样的技巧.(虽然我引用了Lua,但我在Javascript中看到过类似的黑客攻击).

为什么需要进行此优化:

例如,代码

for i = 1, 1000000 do 
   local x = math.sin(i) 
end
Run Code Online (Sandbox Code Playgroud)

运行速度比这个慢30%:

local sin = math.sin 
for i = 1, 1000000 do
    local x = sin(i) 
end
Run Code Online (Sandbox Code Playgroud)

他们在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
Run Code Online (Sandbox Code Playgroud)

这里发生了什么,人们必须做编译器的工作?编译器是如何迷惑的format?为什么这是程序员必须处理的问题?为什么这在1993年没有得到照顾?


我似乎也遇到了一个逻辑悖论:

  1. 如果没有分析,就不应该进行优化
  2. Lua没有能力进行分析
  3. Lua不应该被优化

Mic*_*man 34

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

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

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

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

LuaJIT的情况有点不同 - 它使用跟踪和一些高级魔术来查看代码在运行时所做的事情,因此它实际上可以通过在循环外部移动表达式来实现优化循环,以及除了实际编译之外的其他优化它加工代码,快速疯狂.

关于'重新声明变量为本地' - 在定义模块时很多次,您希望使用原始函数.当访问pairs,max使用他们的全局变量或任何东西,没有人可以向你保证,这将是相同的功能,每一个电话.例如,stdlib重新定义了许多全局函数.

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

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

规则#1:不要这样做.

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

  • 编译器无法弄明白,因为代码不会一直保留在Lua中 - 您可以从Lua调用已注册的C函数,这可以反过来修改Lua环境.这些函数通常来自模块(共享库).你真的期望Lua编译器(总共大约200Kb)反编译库来"搞清楚"吗? (11认同)

jpj*_*obs 11

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

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

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

  • @Ian Boyd,在 Lua 中,“math.sin”的含义*可以*在函数运行时改变。在某些情况下,这种能力很有价值。众所周知的库函数的具体情况是,它不一定那么有价值,但仍然有可能,因此编译器必须尊重您实际编写的代码。 (2认同)
  • @Ian Boyd,存储在名为“math.sin”的全局变量中的函数可以在调用时更改该变量本身。Lua 附带的普通版本无法做到这一点,但*它可以*。编译器必须假设这是允许的(并且很少甚至有用)行为。习惯 Lua 的一件棘手的事情是函数实际上是存储在变量中的值。 (2认同)

kik*_*ito 9

我在Lua环境中反复看到它,我正在摆弄; 人们将变量重新声明为本地变量:

默认情况下这样做是完全错误的.

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

local sin = math.sin 
for i = 1, 1000000 do
  local x = sin(i) 
end
Run Code Online (Sandbox Code Playgroud)

但是,在外部循环中,添加表访问的开销完全可以忽略不计.

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

因为上面提到的两个代码示例并不完全相同.

在我的函数运行时,它不像函数可以改变.

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
Run Code Online (Sandbox Code Playgroud)