【问题标题】:Is it better to declare a local inside or outside a loop? [duplicate]在循环内部或外部声明本地更好吗? [复制]
【发布时间】:2015-09-14 01:02:38
【问题描述】:

我习惯这样做:

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 次不如只创建一次并在每次迭代中重用它的效率。

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

【问题讨论】:

  • “创建局部变量”是在编写源代码时所做的事情。我认为,运行时发生的事情对你和我一样未知。 (好吧,我玩过luac -l 并阅读了一些关于VM 指令集的信息。)

标签: performance lua


【解决方案1】:

所有局部变量都是在编译 (load) 时“创建”的,并且只是函数激活记录的 locals 块的索引。每次定义 local 时,该块增加 1。每次 do..end/lexical 块结束时,它就会收缩。峰值用作总大小:

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 确定,而不是在运行时确定)。

关于你的情况,locals 块大小没有区别,而且,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]-cmets 是我的。

【讨论】:

    【解决方案2】:

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

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

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

    换一种说法,代码如下:

    local a = <some expression>
    

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

    load gp_register, <result from expression>
    

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

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

    当您开始在更广泛(较少局部)范围内创建变量时,与您的想法相反,您实际上可能减慢代码。当您从表面上执行此操作时,您有点与编译器的寄存器分配作斗争,并使编译器更难确定要分配哪些寄存器。在这种情况下,编译器可能会将更多变量溢出到堆栈中,这会降低效率并且实际上会产生附加成本。智能编译器可能仍会生成同样高效的代码,但您实际上可以让事情变得更慢。在这里帮助编译器通常意味着在更小的范围内使用更多的局部变量,从而获得最佳的效率。

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

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

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

    【讨论】:

    • 确实如此。当我查看local a; for i=1,100000000 do a = i * 3 end 时,我可以看到它根本没有做任何事情。我知道编译器编写者比我聪明,所以如果 C++ 编译器将类似的东西优化为 NOP,我不会感到惊讶。
    【解决方案3】:

    与任何性能问题一样,首先要衡量。 在 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。但是,这不是使用它的理由,使用最本地的范围,因为它更具可读性(这是所有语言的好风格:请参阅C 这个问题, JavaC#)

    如果您重用一个表而不是在循环中创建它,那么可能会有更显着的性能差异。在任何情况下,尽可能衡量并支持可读性。

    【讨论】:

    • 正确。可读性比微不足道的性能调整重要 100 倍。
    • 冷跑!在 shell 中执行 first &amp;&amp; second 几次,您会发现没有严格更快。 **1253**/1022, 1020/1022, 1023/1019, 1020/1021, 1022/1020, 1022/1022, 1028/1019, 1021/1021, 1021/1022, ...
    • 其实这两种情况下激活记录中都有两个预定义的slot,操作码没有区别。
    • @user3125367 由于a 默认初始化为nil,因此操作码的第一个区别仅在于额外的LOADNIL。关键是性能差异微不足道,第二个更利于可读性。
    • 您现在可以在我的答案中看到它(并自己检查)。
    【解决方案4】:

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

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

    【讨论】:

    • 对不起,我很困惑。当您说“是的,效率较低”时,您指的是哪一个?在这两个变量中,每次迭代都会重新定义值。
    • 很抱歉给您带来了困惑。我在写“是的,效率较低”时提到了变量的循环内声明。
    猜你喜欢
    • 2010-12-25
    • 2012-02-06
    • 2010-09-27
    • 2016-12-05
    • 1970-01-01
    • 2014-03-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多