Performance 在循环内还是在循环外声明局部变量更好?

Performance 在循环内还是在循环外声明局部变量更好?,performance,lua,Performance,Lua,我习惯于这样做: do local a for i=1,1000000 do a = <some expression> <...> --do something with a end end do 本地a 对于i=11000000 do a= --带着一个微笑做某事 结束 结束 而不是 for i=1,1000000 do local a = <some expression> &l

我习惯于这样做:

do
    local a
    for i=1,1000000 do
        a = <some expression>
        <...> --do something with a
    end
end
do
本地a
对于i=11000000 do
a=
--带着一个微笑做某事
结束
结束
而不是

for i=1,1000000 do
    local a = <some expression>
    <...> --do something with a
end
i=11000000 do的

本地a=
--带着一个微笑做某事
结束
我的理由是,创建一个局部变量1000000次要比只创建一次并在每次迭代中重用它效率低


我的问题是:这是真的还是我遗漏了另一个技术细节?我这样问是因为我没有看到有人这样做,但我不确定原因是因为优势太小还是事实上更糟。“更好”是指使用更少的内存和运行更快。

首先注意:在循环中定义变量可以确保在循环的一次迭代后,下一次迭代不能再次使用相同的存储变量。在for循环之前定义它可以使变量通过多次迭代,就像循环中未定义的任何其他变量一样


此外,回答您的问题:是的,它效率较低,因为它会重新启动变量。如果Lua-JIT-/编译器具有良好的模式识别能力,那么可能只是重置了变量,但我不能确认也不能否认这一点。

与任何性能问题一样,首先测量。 在unix系统中,您可以使用时间:

time lua -e 'local a; for i=1,100000000 do a = i * 3 end'
time lua -e 'for i=1,100000000 do local a = i * 3 end'
输出:

 real   0m2.320s
 user   0m2.315s
 sys    0m0.004s

 real   0m2.247s
 user   0m2.246s
 sys    0m0.000s
更本地的版本在Lua中似乎要快一小部分,因为它不会将
a
初始化为nil。但是,这并不是使用它的理由,使用最局部的范围,因为它更具可读性(这在所有语言中都是很好的样式:请参见此问题, ,及)


如果重用表而不是在循环中创建表,则可能存在更显著的性能差异。在任何情况下,尽可能地衡量和支持可读性。

我认为编译器处理变量的方式有些混乱。从高层次的人类角度来看,定义和销毁一个变量以产生某种“成本”是很自然的

然而,对于优化编译器来说,情况并不一定如此。在高级语言中创建的变量更像是内存中的临时“句柄”。编译器查看这些变量,然后将其转换为中间表示形式(更接近机器的东西),并找出存储所有内容的位置,主要目标是分配寄存器(CPU使用的最直接的内存形式)。然后,它将IR转换为机器代码,在机器代码中甚至不存在“变量”的概念,只存储数据的位置(寄存器、缓存、dram、磁盘)

这个过程包括为多个变量重用相同的寄存器,前提是它们不会相互干扰(前提是它们不是同时需要的:不是同时“活动的”)

换句话说,代码如下:

local a = <some expression>
locala=
生成的程序集可能类似于:

load gp_register, <result from expression>
加载gp\U寄存器,
。。。或者它可能已经有了寄存器中某个表达式的结果,变量最终完全消失了(只是对它使用相同的寄存器)

。。。这意味着变量的存在没有“成本”。它只是直接转换为始终可用的寄存器。“创建寄存器”没有“成本”,因为寄存器总是存在的

当你开始在一个更大的范围(更少的局部)创建变量时,与你所想的相反,你实际上可能会减慢代码的速度。当你表面上这么做的时候,你是在对抗编译器的寄存器分配,这使得编译器很难弄清楚为什么分配什么寄存器。在这种情况下,编译器可能会将更多的变量溢出到堆栈中,这会降低效率,并且实际会增加成本。智能编译器仍然可以发出同样有效的代码,但实际上可以使事情变得更慢。在这里,帮助编译器通常意味着在较小的范围内使用更多的局部变量,在较小的范围内,您有最好的机会提高效率

在汇编代码中,尽可能重用相同的寄存器可以有效地避免堆栈溢出。在带有变量的高级语言中,情况正好相反。减少变量的作用域有助于编译器确定哪些寄存器可以重用,因为对变量使用更局部的作用域有助于告知编译器哪些变量不是同时存在的

<>现在,当你开始使用C++语言中的用户定义的构造函数和析构函数逻辑时,会有一些例外,其中重用对象可能会防止重复使用和破坏可重用的对象。但这不适用于Lua这样的语言,在Lua中,所有变量基本上都是普通的旧数据(或垃圾收集数据或用户数据的句柄)

使用更少的局部变量,您可能会看到改进的唯一情况是,这会以某种方式减少垃圾收集器的工作量。但如果你只是简单地重新分配给同一个变量,情况就不会是这样了。要做到这一点,您必须重用整个表或用户数据(无需重新分配)。换句话说,在某些情况下,重用表的相同字段而不重新创建一个全新的字段可能会有所帮助,但重用用于引用表的变量不太可能有帮助,实际上可能会妨碍性能。

所有局部变量都是在编译时“创建”的(
load
)时间和时间只是索引到函数激活记录的局部块中。每次定义一个
local
,该块将增长1。每次
do..end
/词法块结束时,它都会缩回。峰值用作总尺寸:

function ()
    local a        -- current:1, peak:1
    do
        local x    -- current:2, peak:2
        local y    -- current:3, peak:3
    end
                   -- current:1, peak:3
    do
        local z    -- current:2, peak:3
    end
end
上述函数有3个本地插槽(在加载时确定,而不是在运行时确定)

关于您的情况,本地块大小没有差异,而且,$ luac -l - local a; for i=1,100000000 do a = i * 3 end ^D main <stdin:0,0> (7 instructions, 28 bytes at 0x7fee6b600000) 0+ params, 5 slots, 0 upvalues, 5 locals, 3 constants, 0 functions 1 [1] LOADK 1 -1 ; 1 2 [1] LOADK 2 -2 ; 100000000 3 [1] LOADK 3 -1 ; 1 4 [1] FORPREP 1 1 ; to 6 5 [1] MUL 0 4 -3 ; - 3 // [0] is a 6 [1] FORLOOP 1 -2 ; to 5 7 [1] RETURN 0 1
$  luac -l -
for i=1,100000000 do local a = i * 3 end
^D
main <stdin:0,0> (7 instructions, 28 bytes at 0x7f8302d00020)
0+ params, 5 slots, 0 upvalues, 5 locals, 3 constants, 0 functions
        1       [1]     LOADK           0 -1    ; 1
        2       [1]     LOADK           1 -2    ; 100000000
        3       [1]     LOADK           2 -1    ; 1
        4       [1]     FORPREP         0 1     ; to 6
        5       [1]     MUL             4 3 -3  ; - 3       // [4] is a
        6       [1]     FORLOOP         0 -2    ; to 5
        7       [1]     RETURN          0 1