在循环内部或外部声明变量哪一个更好?
我习惯这样做:
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次比只创建一次并在每次迭代中重用它的效率低。
我的问题是:这是真的还是我漏掉了另一个技术细节?我之所以问这个问题是因为我没有看到任何人这样做,但不确定原因是因为优势太小还是因为实际上更糟。更好的意思是使用更少的内存和更快地运行。
像任何性能问题一样,首先要进行度量。
在 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。然而,这不是使用它的理由,使用最本地的范围是更可读的(这是所有语言都应该遵循的良好风格:参见该问题关于 C、Java 和 C#)。
如果您正在重用表而不是在循环中创建新表,则很可能存在更显著的性能差异。无论如何,保持可读性并度量性能。
我认为人们对编译器处理变量的方式有些困惑。从高层次的人类角度来看,定义和销毁变量似乎会有一些“成本”与之相关联。
但是对于优化编译器来说,情况并非如此。在高级语言中创建的变量更像是临时的内存“句柄”。编译器查看这些变量,然后将其转换为一个中间表示(更接近于机器),并确定要存储所有东西的位置,主要目的是分配寄存器(CPU使用的最直接形式的内存)。然后将IR转换为机器码,其中“变量”的概念甚至不存在,只有存储数据的位置(寄存器,高速缓存,DRAM,磁盘)。
这个过程包括重复使用相同的寄存器来存储多个变量,前提是它们彼此不干扰(只要它们不需要同时使用,即不同时“存在”)。
换句话说,像这样的代码:
local a = <some expression>
生成的汇编可能是这样的:
load gp_register, <result from expression>
或者可能已经在寄存器中具有某些表达式的结果,变量完全消失(只用相同的寄存器),这意味着存在变量没有任何“成本”。它直接转换为寄存器,该寄存器始终可用。 “创建寄存器”的成本也不存在,因为寄存器总是存在的。
当您开始在更广泛的(较少局部的)范围内创建变量时,与您想的相反,可能会导致代码_减慢_。当您表面上这样做时,您正在与编译器的寄存器分配斗争,并使编译器更难以弄清楚为什么寄存器可以分配给何种变量。在这种情况下,编译器可能会将更多的变量溢出到堆栈中,这是不太高效的,并实际上具有成本。智能编译器仍可能发出同样高效的代码,但您可能实际上会使事情变_慢_。在这里帮助编译器通常意味着在更小的范围内使用更多的局部变量,这样您就有最佳的效率机会。
在汇编代码中,尽可能重用相同的寄存器以避免堆栈溢出是有效的。在具有变量的高级语言中,情况正好相反。减少变量的范围_有助于_编译器确定它可以重用哪些寄存器,因为使用更局部的变量范围有助于通知编译器哪些变量不会同时“存在”。
现在,在涉及诸如C ++之类的语言中的用户定义的构造函数和析构函数逻辑时,存在一些例外情况,其中重用_对象_可能会防止不必要的对象构造和销毁可以被重用的对象。但是在像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个局部变量槽(在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]的注释是我的。
- 如何将两个不同的lua文件合成一个 东西有点长 大佬请耐心看完 我是小白研究几天了都没搞定
- 如何在roblox studio中1:1导入真实世界的地形?
- 求解,lua_resume的第二次调用继续执行协程问题。
- 【上海普陀区】内向猫网络招募【Skynet游戏框架Lua后端程序员】
- SF爱好求教:如何用lua实现游戏内调用数据库函数实现账号密码注册?
- Lua实现网站后台开发
- LUA错误显式返回,社区常见的规约是怎么样的
- lua5.3下载库失败
- 请问如何实现文本框内容和某个网页搜索框内容连接,并把网页输出来的结果反馈到另外一个文本框上
- lua lanes多线程使用
- 一个kv数据库
- openresty 有没有比较轻量的 docker 镜像
- 想问一下,有大佬用过luacurl吗
- 在Lua执行过程中使用Load函数出现问题
- 为什么 neovim 里没有显示一些特殊字符?
- Lua比较两个表的值(不考虑键的顺序)
- 有个lua简单的项目,外包,有意者加微信 liuheng600456详谈,最好在成都
- 如何在 Visual Studio 2022 中运行 Lua 代码?
- addEventListener 返回 nil Lua
- Lua中获取用户配置主目录的跨平台方法
请首先注意:在循环内定义变量可以确保在一个迭代后,下一次迭代不能再次使用相同的存储变量。如果在 for 循环之前定义它,则可以将变量在多个迭代中传递,就像未在循环内定义的任何其他变量一样。
此外,为了回答您的问题:是的,这样做效率较低,因为它重新初始化变量。如果 Lua JIT/编译器具有良好的模式识别能力,那么它可能只会重置变量,但我无法证实或否认这一点。