一、lua数据结构及内存占用分析
1.基础数据结构
lua的基本数据表示是type+union的方式,根据不同类型映射到union的不同结构上面,统一的表示结构lua_TValue:
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
long long i; /* lua_Integer integer numbers */
double n; /* lua_Number float numbers */
} Value;
struct lua_TValue {
Value value_;
int tt_;
} TValue;
lua的table占用内存明显高于C/C++,主要有以下几个原因:
1、lua table支持动态插入,所以为了性能必须要分配更大内存,减少因为每次的插入而导致的重新分配。这样极端情况就会多消耗掉一倍的内存。
2、lua table的节点使用的是Node,为了追求通用性,对应kv字段基本都是TValue类型,而这个类型占用16字节,kv消耗加起来就是32字节,明显高于C/C++里面简单字段类型的消耗。当然lua table引入array可以不需要key字段,内存接近省一半。
3、lua table本身的管理数据有56字节,如果是一个很大的表,这个占用比率并不明显。但如果是多个小表,占用比例就会很大。
4、在实际开发过程中,一般都会表嵌套表,很多层,由于每层都有内存冗余和浪费,这样嵌套下来消耗就会叠加的更明显。
collectgarbage("stop");--先停止GC
local gc1 = collectgarbage("count");
local nullTable = {};
local gc2 = collectgarbage("count");
print( gc2 - gc1 );--一个空表是56B
local gc3_1 = collectgarbage("count");
nullTable["a"] = 0;
local gc3_2 = collectgarbage("count");
print( gc3_2 - gc3_1 );--一个kv是32B
local gc4_1 = collectgarbage("count");
nullTable[1] = 0;
local gc4_2 = collectgarbage("count");
print( gc4_2 - gc4_1 );--一个v是16B
collectgarbage("restart");--重新唤起GC
2.lua内存占用分析
Lua中table类型,每条记录对外都是key-value的方式读写,底层是用array+hashtable的方式管理数据的,但对外是透明的。
不论array还是hashtable都是连续的内存分布。在查找时:
1. 如果key是整型, 并且 key > 1 and key < max_array_size, 直接取array[key]数据
2. 其他情况,默认读取hashtable。Hashtable的管理方有些特别,当不同key hash到同一个node时,用链表来维护这些冲突节点。与stringtable 链表节点动态分配的方法不同, HashTable使用空闲链表来维护冲突节点。
首先说一种典型的情况,调用table.insert 或者table[#table + 1] 按序插入列表的,就是存放在array里面。
使用预填充方式创建table会省CPU消耗,否则每次动态扩容都会新建数据表,把原来的数据重新hash到新分配的内存中,并且每次扩容都是上一次的2倍。
rehash数据重新分布
上面只是描述节点不够用时触发内存扩容,数据重新进行hash分布。但如果既有Array数据,又有HashTable数据时,会怎么进行rehash呢?
系统会把所有的key为整数的节点进行统计(包含在array和HashTable的),同时数组最大内存Max_Aarry_Size按2的指数倍增长,然后计算满足条件Key <=Max_Aarry_Size的整数节点数量,当数量 > Max_Aarry_Size/2 就认为array内存能充分利用,使用率超过50%,直到系统找到一个最大的符合条件的Max_Aarry_Size为止。剩下的节点数量进入HashTable, HashTable也是按2的指数倍增长,直到能够装下剩余节点数为止。
因此,就可知道下方的表为什么使用的HashTable存储的原因了。
for i=1, 10000 do
tab[10000+i] = 10000+i
end
当Max_Aarry_Size = 2^13=8196时,没有Key落在这个[1, 8196]区间。
当Max_Aarry_Size= 2^14=16392时,只有6392条数据落入区间[1, 16392], 区间利用率小于50%。
当Max_Aarry_Size= 2^15=32784时,只有10000落入区间[1, 32784], 也不满足利用率的要求。
二、Lua高级应用
1.Lua协程
---唤起协程
function receive (prod)
local status, value = coroutine.resume(prod)
return value
end
---挂起等待
function send (x)
coroutine.yield(x)
end
---生产者:生产一个产品
function producer ()
return coroutine.create(function ()
while true do
local x = math.random( 1, 1000 ); -- produce new value
send(x)
end
end)
end
---解释器:对产品进行包装操作
function filter (prod)
return coroutine.create(function ()
local line = 1
while true do
local x = receive(prod) -- get new value
x = string.format("Line: %5d Value: %s", line, x)
send(x) -- send it to consumer
line = line + 1
end
end)
end
---消费者:消费者有需求,则通知解释器,解释器先通知生产者生产,再进行包装,交给消费者
function consumer (prod)
return receive(prod) -- get new value
end
local product = filter( producer());
function TestPanel.secondUpdate()
print_green("-----",consumer( product ));
end
2.非抢占式多线程:LuaSocket库
require "luasocket"; ---加载LuaSocket库
---接收数据
function receive (connection)
connection:timeout(0) -- timeout(0)使得对连接的任何操作都不会阻塞
local s, status = connection:receive(2^10)
--当操作返回的状态为 timeout 时意味着操作未完成就返回了
if status == "timeout" then --timeout的时候挂起,挂起后s和status还是在被不断推送的。
coroutine.yield(connection)
end
return s, status
end
---下载数据
function download (host, file)
local c = assert(socket.connect(host, 80)) --80主机端口
local count = 0 -- counts number of bytes read
c:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
while true do
local s, status = receive
count = count + string.len(s)
if status == "closed" then break end
end --上述方法只是计算接收到的文件大小(字节数)
c:close() --关闭连接
print(file, count)
end
---获取文件
threads = {} -- list of all live threads
function get (host, file) -- create coroutine
local co = coroutine.create(function ()
download(host, file)
end)
-- insert it in the list
table.insert(threads, co)
end
---分配器( 对所有线程循环,移除掉已经完成任务的线程)
function dispatcher ()
while true do
local n = #threads
if n == 0 then break end -- no more threads to run
local connections = {}
for i=1,n do
local status, res = coroutine.resume(threads[i])
if not res then -- thread finished its task?
table.remove(threads, i)
break
else -- timeout
table.insert(connections, res)
end
end
if #connections == n then --当所有的连接都 timeout 分配器调用 select 等待任一连接状态的改变,此方法不会发生忙等待,否则可能会引起程序阻塞。
socket.select(connections)
end
end
end
---实例调用
function GetSocketData()
local host = "www.w3c.org";
get(host, "/TR/html401/html40.txt");
get(host, "/TR/2002/REC-xhtml1-20020801/xhtml1.pdf");
get(host, "/TR/REC-html32.html");
get(host, "/TR/2000/REC-DOM-Level-2-Core-20001113/DOM2-Core.txt");
dispatcher() --main loop;
end
3.设置只读表
function readOnly( t )
local proxy = {};
local mt = {
__index = t,
__newindex = function( t, k , v )
print_e( "attempt to update a read-only table" );
end
}
setmetatable( proxy , mt );
return proxy;
end
4.使用动态名字访问全局变量
--获取值
function getfield(f )
local v = _G; -- lua会把所有全局表存到_G里
for w in string.gmatch(f, "[%w_]+") do
v = v[w];--层层查找
end
return v;--返回最后一层找到的值
end
--设置值
function setfield(f, v )
local t = _G
for w,d in string.gmatch(f, "([%w_]+)(.?)") do
if d == "." then
t[w] = t[w] or {}
t = t[w]
else
t[w] = v
end
end
end
setfield("TestManager.test.value", 10 );
print( tostring( getfield( "TestManager.test.value" ) ) );--10
local a = TestManager.test.value; --10
5.使用二进制节省内存
一个空的table ({})就会占用56字节,另外每个键占用16字节,每个值占用16字节,加起来32字节,如果是连续的键(1,2,3...) , 那样就只有值占用16字节。 所以应该尽量避免频繁的创建table,尤其是在update里。另外假如想弄一个字段,但是一个键对应两个值,一般写法是: table = { [key] = { value1 = 1 , value2 = 2 } }; 可以看出每个key都要创建个table但是里面就存了2个值,性价比太低,可以优化成 table = { [key] = ( 1 << 16 ) + 2 };(因为位运算优先级较低,所以需要加个括号)这样每个key就对应一个数字,内存比table小得多。
值的取法:取 value里的1 : value >> 16 (因为1左移了16位,取的时候只要右移16位就行了) 。 取value里的2 : value & 0xFFFF。 ( 0xFFFF 是16进制写法,0x是固定格式,FFFF对应二进制的 1111 1111 1111 1111,当value和 0xFFFF进行按位与运算后,得出的就是后16位的值,即为2,(因为1左移了16位,与不到) )
(当然16位不够也可以32 , 但是不要超过32,lua的number使用double类型,故有53个有效位,外加1个隐含位和1个符号位 , 左移的时候注意位数 )
简单说一下二进制与16进制转换:以二进制的每4位为一个单位,1111 , 分别对应 8 4 2 1 ,即2^3 2^2 2^1 2^0 。转化为十进制就是 1+2+4+8 = 15 , 即十六进制的F。 1111 1111 1111 1111就是十六进制的FFFF。同理 二进制1100 => 8 + 4 + 0 + 0 = 十进制12 =》 十六进制C 0101 => 0+4+0+1 =》 十进制5 =》十六进制5 。