在循环内部或外部声明变量哪一个更好?

我习惯这样做:

do
    local a
    for i=1,1000000 do
        a = <some expression>
        <...> --对a执行某些操作
    end
end

而不是

for i=1,1000000 do
    local a = <some expression>
    <...> --对a执行某些操作
end

我的理由是,创建一个本地变量1000000次比只创建一次并在每次迭代中重用它的效率低。

我的问题是:这是真的还是我漏掉了另一个技术细节?我之所以问这个问题是因为我没有看到任何人这样做,但不确定原因是因为优势太小还是因为实际上更糟。更好的意思是使用更少的内存和更快地运行。

点赞
用户5054939
用户5054939

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

此外,为了回答您的问题:是的,这样做效率较低,因为它重新初始化变量。如果 Lua JIT/编译器具有良好的模式识别能力,那么它可能只会重置变量,但我无法证实或否认这一点。

2015-06-27 00:43:56
用户2726734
用户2726734

像任何性能问题一样,首先要进行度量。 在 Unix 系统中,您可以使用 time

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。然而,这不是使用它的理由,使用最本地的范围是更可读的(这是所有语言都应该遵循的良好风格:参见该问题关于 CJavaC#)。

如果您正在重用表而不是在循环中创建新表,则很可能存在更显著的性能差异。无论如何,保持可读性并度量性能。

2015-06-27 01:01:38
用户88888888
用户88888888

我认为人们对编译器处理变量的方式有些困惑。从高层次的人类角度来看,定义和销毁变量似乎会有一些“成本”与之相关联。

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

这个过程包括重复使用相同的寄存器来存储多个变量,前提是它们彼此不干扰(只要它们不需要同时使用,即不同时“存在”)。

换句话说,像这样的代码:

local a = <some expression>

生成的汇编可能是这样的:

load gp_register, <result from expression>

或者可能已经在寄存器中具有某些表达式的结果,变量完全消失(只用相同的寄存器),这意味着存在变量没有任何“成本”。它直接转换为寄存器,该寄存器始终可用。 “创建寄存器”的成本也不存在,因为寄存器总是存在的。

当您开始在更广泛的(较少局部的)范围内创建变量时,与您想的相反,可能会导致代码_减慢_。当您表面上这样做时,您正在与编译器的寄存器分配斗争,并使编译器更难以弄清楚为什么寄存器可以分配给何种变量。在这种情况下,编译器可能会将更多的变量溢出到堆栈中,这是不太高效的,并实际上具有成本。智能编译器仍可能发出同样高效的代码,但您可能实际上会使事情变_慢_。在这里帮助编译器通常意味着在更小的范围内使用更多的局部变量,这样您就有最佳的效率机会。

在汇编代码中,尽可能重用相同的寄存器以避免堆栈溢出是有效的。在具有变量的高级语言中,情况正好相反。减少变量的范围_有助于_编译器确定它可以重用哪些寄存器,因为使用更局部的变量范围有助于通知编译器哪些变量不会同时“存在”。

现在,在涉及诸如C ++之类的语言中的用户定义的构造函数和析构函数逻辑时,存在一些例外情况,其中重用_对象_可能会防止不必要的对象构造和销毁可以被重用的对象。但是在像Lua这样的语言中,所有变量基本上都是普通数据(或进入垃圾收集数据或用户数据的句柄)。

唯一可能看到减少局部变量使用会带来改善的情况是,如果此举在垃圾收集器方面减少了工作。但是,如果您只是简单地重新分配同一变量,那么情况并非如此。要做到这一点,您必须重复使用整个表格或用户数据(而不是重新分配)。换句话说,在不重新创建全新表格的情况下重复使用表格的相同字段可能在某些情况下有所帮助,但重复使用用于引用表格的变量非常不可能有所帮助,实际上可能会影响性能。

2015-06-27 02:19:48
用户3125367
用户3125367

所有的局部变量都是在编译(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个局部变量槽(在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

$  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]的注释是我的。

2015-06-27 15:27:46