Lua Performance Tips给出了很重要的性能优化建议,这些建议都是用Lua编程时需要时刻注意的事项。
本页内容可以到Lua编程时性能方面的注意事项中分章节查看。
Lua代码是被解释执行的,Lua代码被解释成Lua虚拟机的指令,交付给Lua虚拟机执行。
Lua虚拟机不是对常见的物理计算机的模拟,而是完全定义了自己的规则。Lua虚拟机中虚拟了寄存器,但是它的寄存器是和函数绑定的一起的,并且每个函数可以使用高达250个虚拟寄存器(使用栈的方式实现的)。
操作局部变量时,使用的指令非常少,例如a=a+b只需要一条指令ADD 0 0 1。
如果a和b是全局变量,则需要四条指令:
GETGLOBAL       0 0     ; a
GETGLOBAL       1 1     ; b
ADD             0 0 1
SETGLOBAL       0 0     ; a
下面两段代码var_global.lua和var_local.lua性能相差30%:
var_global.lua:
for i = 1, 1000000 do
local x = math.sin(i)
end
var_local.lua:
local sin = math.sin
for i = 1, 1000000 do
local x = sin(i)
end
运行耗时占比:
➜  03-performance git:(master) ✗ time lua5.1 ./var_global.lua
lua5.1 ./var_global.lua  8.02s user 0.01s system 99% cpu 8.036 total
➜  03-performance git:(master) ✗ time lua5.1 ./var_local.lua
lua5.1 ./var_local.lua  6.01s user 0.01s system 99% cpu 6.026 total
因此在阅读使用lua代码时,经常看到下面的做法,将其它包中的变量赋值给本地变量:
local sin = math.sin
function foo (x)
  for i = 1, 1000000 do
    x = x + sin(i)
  end
  return x
end
print(foo(10))
Lua中可以用load、load和loadstring动态加载代码并执行。应当尽量避免这种用法,动态加载代码需要被立即编译,编译开销很大。
load_dynamic.lua:
local lim = 10000000
local a = {}
for i = 1, lim do
	a[i] = loadstring(string.format("return %d", i))
end
print(a[10]())  --> 10
load_static.lua:
function fk (k)
	return function () return k end
end
local lim = 10000000
local a = {}
for i = 1, lim do
	a[i] = fk(i)
end
print(a[10]())  --> 10
上面两段代码的性能完全不同,后者耗时只有前者的十分之一:
➜  03-performance git:(master) ✗ time lua-5.1 ./load_dynamic.lua
10
lua-5.1 ./load_dynamic.lua  47.99s user 3.81s system 99% cpu 52.069 total
➜  03-performance git:(master) ✗ time lua-5.1 ./load_static.lua
10
lua-5.1 ./load_static.lua  4.66s user 0.62s system 99% cpu 5.308 total
Lua的table是由数组部分(array part)和哈希部分(hash part)组成。数组部分索引的key是1~n的整数,哈希部分是一个哈希表(open address table)。
向table中插入数据时,如果已经满了,Lua会重新设置数据部分或哈希表的大小,容量是成倍增加的,哈希部分还要对哈希表中的数据进行整理。
需要特别注意的没有赋初始值的table,数组和部分哈希部分默认容量为0。
local a = {}     --容量为0
a[1] = true      --重设数组部分的size为1
a[2] = true      --重设数组部分的size为2
a[3] = true      --重设数组部分的size为4
local b = {}     --容量为0
b.x = true       --重设哈希部分的size为1
b.y = true       --重设哈希部分的size为2
b.z = true       --重设哈希部分的size为4
因为容量是成倍增加的,因此越是容量小的table越容易受到影响,每次增加的容量太少,很快又满。
对于存放少量数据的table,要在创建table变量时,就设置它的大小,例如:
table_size_predefined.lua:
for i = 1, 1000000 do
  local a = {true, true, true}   -- table变量a的size在创建是确定
  a[1] = 1; a[2] = 2; a[3] = 3   -- 不会触发容量重设
end
如果创建空的table变量,插入数据时,会触发容量重设,例如:
table_size_undefined.lua:
for i = 1, 1000000 do
  local a = {}                   -- table变量a的size为0
  a[1] = 1; a[2] = 2; a[3] = 3   -- 触发3次容量重设
end
后者耗时几乎是前者的两倍:
➜  03-performance git:(master) ✗ time lua-5.1 table_size_predefined.lua
lua-5.1 table_size_predefined.lua  4.17s user 0.01s system 99% cpu 4.190 total
➜  03-performance git:(master) ✗ time lua-5.1 table_size_undefined.lua
lua-5.1 table_size_undefined.lua  7.63s user 0.01s system 99% cpu 7.650 total
对于哈希部分也是如此,用下面的方式初始化:
local b = {x = 1, y = 2, z = 3}
table只有在满的情况下,继续插入的数据的时候,才会触发rehash。如果将一个table中的数据全部设置为nil,后续没有插入操作,这个table的大小会继续保持原状,不会收缩,占用的内存不会释放。 除非不停地向table中写入nil,写入足够多的次数后,重新触发rehash,才会发生收缩:
a = {}
lim = 10000000
for i = 1, lim do a[i] = i end            --  create a huge table
print(collectgarbage("count"))            --> 196626
for i = 1, lim do a[i] = nil end          --  erase all its elements
print(collectgarbage("count"))            --> 196626,不会收缩
for i = lim + 1, 2*lim do a[i] = nil end  --  create many nil element
print(collectgarbage("count"))            --> 17,添加足够多nil之后才会触发rehash
不要用这种方式触发rehash,如果想要释放内存,就直接释放整个table,不要通过清空它包含的数据的方式进行。
将table中的成员设置为nil的时候,不触发rehash,是为了支持下面的用法:
for k, v in pairs(t) do
  if some_property(v) then
    t[k] = nil    -- erase that element
  end
end
如果每次设置nil都触发rehash,那么上面的操作就是一个灾难。
如果要在保留table变量的前提下,清理table中的所有数据,一定要用pairs()函数,不能用next()。
next()函数是返回中的table第一个成员,如果使用下面方式,next()每次从头开始查找第一个非nil的成员:
while true do
	local k = next(t)
	if not k then break end
	t[k] = nil
end
正确的做法是用pairs:
for k in pairs(t) do
	t[k] = nil
end
Lua中的字符串是非常不同的,它们全部是内置的(internalized),或者说是全局的,变量中存放的是字符串的地址,并且每个变量索引的都是全局的字符串,没有自己的存放空间。
例如下面的代码,为变量a和变量b设置了同样的内容的字符串”abc”,”abc”只有一份存在,a和b引用的是同一个:
local a = "abc"
local b = "abc"
如果要为a索引的字符串追加内容,那么会创建一个新的全局字符串:
a = "abc" .. "def"
创建全局字符串的开销是比较大的,在lua中慎用字符串拼接。
如果一定要拼接,将它们写入table,然后用table.contat() 连接起来,如下:
local t = {}
for line in io.lines() do
  t[#t + 1] = line
end
s = table.concat(t, "\n")
假设要存放多边形的多个顶点,每个顶点一个x坐标,一个y坐标。
方式一,每个顶点分配一个table变量:
polyline = { { x = 10.3, y = 98.5 },
             { x = 10.3, y = 18.3 },
             { x = 15.0, y = 98.5 },
             ...
           }
方式二,每个顶点分配一个数组变量,开销要比第一种方式少:
polyline = { { 10.3, 98.5 },
             { 10.3, 18.3 },
             { 15.0, 98.5 },
             ...
           }
方式三,将x坐标和y坐标分别存放到两个数组的中,一共只需要两个数组变量,开销更少:
polyline = { x = { 10.3, 10.3, 15.0, ...},
             y = { 98.5, 18.3, 98.5, ...}
           }
要有意思的的优化代码,尽量少创建变量。
如果在循环内创建变量,那么每次循环都会创建变量,导致不必要的创建、回收:
在循环内创建变量,var_inloop.lua:
function foo ()
	for i = 1, 10000000 do
		local t = {0}
		t[1]=i
	end
end
foo()
在循环外创建变量,var_outloop.lua:
local t={0}
function foo ()
	for i = 1, 10000000 do
		t[1] = i
	end
end
foo()
这两段代码的运行时间不是一个数量级的:
➜  03-performance git:(master) ✗ time lua-5.1 var_inloop.lua
lua-5.1 var_inloop.lua  3.41s user 0.01s system 99% cpu 3.425 total
➜  03-performance git:(master) ✗ time lua-5.1 var_outloop.lua
lua-5.1 var_outloop.lua  0.22s user 0.00s system 99% cpu 0.224 total
	
变量是这样的,函数也是如此:
local function aux (num)
	num = tonumber(num)
	if num >= limit then return tostring(num + delta) end
end
	
for line in io.lines() do
	line = string.gsub(line, "%d+", aux)
	io.write(line, "\n")
end
不要用下面的这种方式:
for line in io.lines() do
	line = string.gsub(line, "%d+", 
	     function (num)
	         num = tonumber(num)
	         if num >= limit then return tostring(num + delta) 
	     end
	)
	io.write(line, "\n")
end
能用分片表示字符串,就不要创建新的字符串。
function memoize (f)
    local mem = {}                       -- memoizing table
    setmetatable(mem, {__mode = "kv"})   -- make it weak
    return function (x)       -- new version of ’f’, with memoizing
        local r = mem[x]
        if r == nil then      -- no previous result?
            r = f(x)          -- calls original function
            mem[x] = r        -- store result for reuse
        end
        return r
    end
end
用memoize()创建的函数的运算结果可以被缓存:
loadstring = memoize(loadstring)
可以通过collectgarbage干预垃圾回收过程,通过collectgarbage可以停止和重新启动垃圾回收。
可以在需要被快速运行完成的地方,关闭垃圾回收,等运算完成后再重启垃圾回收。
Copyright @2011-2019 All rights reserved. 转载请添加原文连接,合作请加微信lijiaocn或者发送邮件: [email protected],备注网站合作
友情链接: Some Online Tools Develop by Me 系统软件 程序语言 运营经验 水库文集 网络课程 微信网文