在循环内部或外部声明局部更好吗?

时间:2015-06-27 00:32:50

标签: performance lua

我习惯这样做:

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

而不是

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

我的理由是,创建局部变量1000000次的效率低于仅创建一次并在每次迭代时重复使用它。

我的问题是:这是真的还是我缺少另一个技术细节?我在问,因为我没有看到有人这样做,但不确定原因是因为优势太小还是因为事实上更糟。更好的是,我的意思是使用更少的内存并且运行得更快。

4 个答案:

答案 0 :(得分:10)

与任何表现问题一样,先测量。 在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初始化为零。但是,没有理由使用它,使用最本地的范围,因为它更具可读性(这是所有语言中的好风格:请参阅此问题CJavaC#

如果您正在重复使用表而不是在循环中创建它,那么可能会有更显着的性能差异。无论如何,只要有可能,就要衡量并支持可读性。

答案 1 :(得分:5)

我认为编译器处理变量的方式存在一些混淆。从高级别的人类角度来看,考虑定义和销毁变量以获得某种“成本”是很自然的。与之相关。

然而,优化编译器并不一定如此。您在高级语言中创建的变量更像是临时的&#34;句柄&#34;进入记忆。编译器查看这些变量,然后将其转换为中间表示(更靠近机器的地方)并找出存储所有内容的位置,主要是为了分配寄存器(CPU最直接的内存形式)。然后它将IR转换为机器代码,其中有一个&#34;变量&#34;甚至不存在,只存储数据(寄存器,缓存,dram,磁盘)。

此过程包括为多个变量重复使用相同的寄存器,前提是它们不会相互干扰(前提是它们不是同时需要的:不是&#34;同时存在&#34;

换句话说,使用以下代码:

local a = <some expression>

生成的程序集可能类似于:

load gp_register, <result from expression>

...或者它可能已经有来自寄存器中某个表达式的结果,并且变量最终完全消失(只是使用相同的寄存器)。

......这意味着没有&#34;成本&#34;变量的存在。它只是直接转换为始终可用的寄存器。没有&#34;成本&#34; to&#34;创建一个寄存器&#34;,因为寄存器总是在那里。

当您开始在更广泛(更少本地)的范围内创建变量时,与您的想法相反,您实际上可能减慢代码。当你表面上这样做时,你就会对编译器的寄存器分配进行反击,并使编译器更难以找出要为什么分配的寄存器。在这种情况下,编译器可能会将更多变量泄漏到堆栈中,这样做效率较低,实际上会附加成本。智能编译器仍然可以发出同样高效的代码,但实际上你可以使事情更慢。在这里帮助编译器通常意味着在较小的范围内使用更多局部变量,在这些变量中,您最有效率。

在汇编代码中,尽可能重用相同的寄存器可以有效避免堆栈溢出。在带有变量的高级语言中,它恰恰相反。减少变量的范围有助于编译器找出它可以重用的寄存器,因为使用更局部的变量范围有助于告知编译器哪些变量不能同时存在。

现在,当您开始在C ++等语言中使用用户定义的构造函数和析构函数逻辑时会出现异常,其中重用对象可能会阻止可重用的对象的冗余构造和破坏。但这并不适用于像Lua这样的语言,其中所有变量基本上都是普通的旧数据(或处理垃圾收集数据或用户数据)。

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

答案 2 :(得分:3)

所有局部变量都是在编译(load)时“创建”的,并且只是索引到函数激活记录的locals块中。每次定义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个本地插槽(在load确定,而不是在运行时)。

关于您的情况,本地块大小没有区别,而且luac / 5.1生成相等的列表(只有索引更改):

$  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

VS

$  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

// [n] - 评论是我的。

答案 3 :(得分:2)

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

此外,回答你的问题:是的,效率较低,因为它会重新启动变量。如果Lua JIT- / Compiler具有良好的模式识别,可能只是重置变量,但我不能确认也不否认。