云风的个人空间 : Lua 编程技巧[LuaTips]

首页 :: 索引 :: 修订历史 :: 你好, 18.191.171.72
你的足迹: » Lua 编程技巧
这是一个旧版本的LuaTips于2006-06-06 16:38:37.
Lua 的 5.1 版本已经正式发布。现在,我们应该把全部讨论放在这个版本上。

Hot! 应该尽量使用 local 变量而非 global 变量。这是 Lua 初学者最容易犯的错误。global 变量实际上是放在一张全局的 table 里的。global 变量实际上是利用一个 string (变量名作 key) 去访问这个 table 。虽然[InterWiki]Lua5 的 table 效率很高 ,但是相对于 local 变量,依然有很大的效率损失。local 变量是直接通过 Lua 的堆栈访问的。有些 global 变量的访问是不经意的,比如我们有双重循环操作一个迭代的 table:
for k1,v1 in pairs(tbl) do
    for k2,v2 in pairs(v1) do
        ...	
    end
end


这里,pairs 其实是一个全局变量应用的函数。如果我们这样做:
do
    local pairs=pairs
    for k1,v1 in pairs(tbl) do
        for k2,v2 in pairs(v1) do
            ...	
        end
    end
end


效率会稍微提高一些。如果是单层循环,这样做就没有意义。因为 for ... in 循环中的 pairs 这个函数只会被调用一次,而不是每次循环都去调。我们的原则其实是,被多次读取的 global 变量,都应该提取出来放到 local 变量中。

Hot! 应该尽量减少函数对 local 变量的可视范围,常用的技巧是使用 do ... end 。当定义一个 function 的时候,这个 function 向上所有可见的范围的 local 变量都会被作为 upvalue 和 function 本身绑定在一起,成为一个 closure 。太多的 local 变量虽然几乎没有速度损失,但是会带来额外的内存消耗。过多的内存消耗会过早引起 gc 。尤其是下面这样的例子:
function foo()
    local a,b,c
    ...
    return function() ... end
end

每次调用 foo() 时返回的 function 都会将它可见的 local 变量绑定在一起,形成新的 closure 。这些 local 变量包括 foo() 内的 a,b,c ,还包括 foo() 上面可见的一些 local 变量。这将有可能成为性能问题。btw, closure 和 function 是两个概念。这里是会产生一份 function ,但是 closure 再每次调用的时候都会产生一份新的。
有些时候,closure 的产生是可以避免的,这里有 [InterWiki]一个例子 我从 [External Link]http://lua-users.org 上摘抄下来:
function create()
    local e = {}
    e.x = 0
    e.update = function(self)    -- mark
        self.x = self.x + 1
    end
    return e
end
 
e1 = create()
e2 = create()
e3 = create()

这里,mark 这行,会被调用三次,每次均产生一个新的 closure 用于绑定 e (不过 function 本身只产生了一份) 。这里,local e 显然对 e.update 这个函数本身没有用,我们只需要把函数提到前面去即可。
do
    local function temp(self)
        self.x = self.x + 1
    end
 
    function create()
        local e = {
            x = 0,
            update = temp
        }
        return e
    end
end

这样,就可以避免 closure 的产生了。因为 function temp 不需要携带任何 upvalue 。btw, 我再这里修改了对 e 这个table 的赋值,这将稍微优于前面的赋值方法。因为编译器会得到 table 的大小,而避免多次分配内存。

如果这个例子扩展开,我们必须让 function temp 知道某些 create 调用时传入的信息,如:
function create(init)
    local e = {
        x = 0,
        reset = function (self) self.x = init end
    }
    return e
end


我们可以使用一个技巧,避免 function temp 绑定 upvalue init 。
do
    local reset
 
    function reset(self)
        self.x = self[reset]
    end
 
    function create(init)
        return {
            x = 0,
            [reset] = init,
            reset = reset
        }
    end
end


这里,把 init 这个值放到了 table 本身,reset 函数可以通过 self 取出来。key 选用的是 function reset ,这绝对可以避免和其它的 key 冲突。唯一的问题是让 table 大了一些。可是,lua table 是 hash 部分是按 2 的幂递增的,通常这不会带来额外的负担。一旦这个额外的 key 真的成了负担,即,增加这么一个 key 就会让 table 变成两倍大小。那么,我们可以用 metatable 解决这个问题。

do
    local funcs = {}
    local meta = {__index = funcs}
 
    function funcs.reset(self)
        self.x = self[funcs]
    end
 
    function create(init)
        local e={
          x = 0,
         [funcs] = init
        }
        setmetatable(e, meta)
        return e
    end
end


Hot! 警惕临时变量 字符串的连接操作,会产生新的对象。这是由 lua 本身的 string 管理机制导致的。lua 在 VM 内对相同的 string 永远只保留一份唯一 copy ,这样,所有字符串比较就可以简化为地址比较。这也是 lua 的 table 工作很快的原因之一。这种 string 管理的策略,跟 java 等一样,所以跟 java 一样,应该尽量避免在循环内不断的连接字符串,比如 a = a..x 这样。每次运行,都很可能会生成一份新的 copy 。

同样,记住,每次构造一份 table 都会多一份 table 的 copy 。比如在 lua 里,把平面坐标封装成 { x, y } 用于参数传递,就需要考虑这个问题。每次你想构造一个坐标对象传递给一个函数,{ 10,20 }  这样明确的写出,都会构造一个新的 table 出来。要么,我们想办法考虑 table 的重用;要么,干脆用 x,y 两个参数传递坐标。

同样需要注意的是以 function foo (...) 这种方式定义函数, ... 这种不定参数,每次调用的时候都会被定义出一个 table 存放不定数量的参数。

这些临时构造的对象往往要到 gc 的时候才被回收,过于频繁的 gc 有时候正是效率瓶颈。

Hot! 使用 closure 代替 table 上面提到封装坐标的问题。诚然,我们可以用 { x=1,y=2 } 这样封装一个坐标。不过还有一个方法可供选择。它稍微轻量一点。

function point (x,y)
	return function () return x,y end
end
 
-- 使用范例
p=point(1,2)
print(p())
-- 输出 1	 2


如果你愿意,还可以做的复杂一点:
function point (x,y)
	return function (idx) 
		if idx=="x" then return x
		elseif idx=="y" then return y
		else return x,y end
	end
end
 
-- 使用范例
p=point(1,2)
print(p("x"))		-- 1
print(p("y"))		-- 2


x,y 实际被存放在 closure 里,每次调用 function point 都有一份独立的 closure。当然,function 的 code 只有一份。

Hot! 设法减少从 C 向 Lua 传递字符串 字符串常量在 Lua VM 内部工作的非常快,但是一个从 C 向 lua vm 通过 lua_pushstring 之类的 api 传递进 VM 时,就需要掂量一下了。这至少包含一个再 hash 和匹配的过程。[InterWiki]我的 Blog 上的一篇文章讨论了这个问题