最近游戏项目改用c++/lua开发,于是开始学习lua,lua是一种轻量小巧的脚本语言,据说lua是最快的脚本语言也不无道理。这篇文章从lua的数据结构入手,把lua的实现描述出来,加深自己的理解。(lua源码版本为5.2.3)
所谓lua虚拟机其实就是一个c的struct结构体(lua_State),所有lua代码都通过解析器加载到lua_State结构中保存。lua中的基础数据类型分为8种:nil, boolean, lightuserdata, number, string, table, function(包括c函数cfunction和lua函数lfunction), userdata, thread, 其中最重要的就是table,因为所有的数据其实都是保存在table中的。程序就是数据结构和算法,那么在lua中是怎么表示这些数据类型呢。
图 1-1
如图1-1所示,lua中对基础数据类型使用统一的数据结构TValue表示,value_表示值,tt_表示数据类型。由此可知Value是一个union结构,结合源码(lobject.h 184行开始/* Macros to set values */)可知,对于nil,boolean,lightuserdata,number,cfunction这些数据类型的值都是直接存放在TValue中,其他类型的数据都用GCObject来表示,TValue中只是保存GCObject结构的指针。下面重点讲一下Table和TString两种类型,后面深入其他东西的时候会继续讲到。
1. TString
/*
** creates a new string object
*/
static TString *createstrobj (lua_State *L, const char *str, size_t l,
int tag, unsigned int h, GCObject **list) {
TString *ts;
size_t totalsize; /* total size of TString object */
totalsize = sizeof(TString) + ((l + 1) * sizeof(char));
ts = &luaC_newobj(L, tag, totalsize, list, 0)->ts;
ts->tsv.len = l;
ts->tsv.hash = h;
ts->tsv.extra = 0;
memcpy(ts+1, str, l*sizeof(char));
((char *)(ts+1))[l] = '\0'; /* ending 0 */
return ts;
}
图1-2
从代码可以看出,字符串在lua的内存分配结构,如图1-2所示。lua字符串都自动加上结束符。
/*
** new string (with explicit length)
*/
TString *luaS_newlstr (lua_State *L, const char *str, size_t l) {
if (l <= LUAI_MAXSHORTLEN) /* short string? */
return internshrstr(L, str, l);
else {
if (l + 1 > (MAX_SIZET - sizeof(TString))/sizeof(char))
luaM_toobig(L);
return createstrobj(L, str, l, LUA_TLNGSTR, G(L)->seed, NULL);
}
}
在实际中对字符串的使用大部分都是很短的,所以lua保存字符串分为短字符串和长字符串,短字符串都保存在全局的字符串hash表中,长字符串则放在全局的可gc对象列表中。
/*
** checks whether short string exists and reuses it or creates a new one
*/
static TString *internshrstr (lua_State *L, const char *str, size_t l) {
GCObject *o;
global_State *g = G(L);
unsigned int h = luaS_hash(str, l, g->seed);
for (o = g->strt.hash[lmod(h, g->strt.size)];
o != NULL;
o = gch(o)->next) {
TString *ts = rawgco2ts(o);
if (h == ts->tsv.hash &&
l == ts->tsv.len &&
(memcmp(str, getstr(ts), l * sizeof(char)) == 0)) {
if (isdead(G(L), o)) /* string is dead (but was not collected yet)? */
changewhite(o); /* resurrect it */
return ts;
}
}
return newshrstr(L, str, l, h); /* not found; create a new string */
}
短字符串的hash表采用开放寻址hash算法,在处理一个短字符串的时候对首先判断字符串在hash表中是否已存在,存在则直接返回其地址;不存在则创建该字符串,并求出其hash值。长字符串则都重新分配内存保存。因此在对比两个字符串是否相等时,短字符串只要比较地址是否相等就行了,而对于长字符串则需要对比所有字符。由此可见lua中对于短字符串的处理很高效,一般用于字符串的比较,或者用作table的key。
2. Table
图1-3
CommonHeader先忽略,lu_byte 是typedef unsigned char lu_byte,然后结合源码来讲解Table的实现(ltable.c, ltable.h),对lua中对table的操作函数接口都定义在ltable.h中。lua中的table是key-value的形式来存放数据的,table分为两部分:数组部分array和hash部分。array和sizearray为数组部分,node,lastfree,lsizenode为hash部分。
Table *luaH_new (lua_State *L) {
Table *t = &luaC_newobj(L, LUA_TTABLE, sizeof(Table), NULL, 0)->h;
t->metatable = NULL;
t->flags = cast_byte(~0);
t->array = NULL;
t->sizearray = 0;
setnodevector(L, t, 0);
return t;
}
创建一个空的table时,数组部分和hash部分都为0。
返回表的一个key的值,以指针的形式返回:
/*
** main search function
*/
const TValue *luaH_get (Table *t, const TValue *key) {
switch (ttype(key)) {
case LUA_TSHRSTR: return luaH_getstr(t, rawtsvalue(key));
case LUA_TNIL: return luaO_nilobject;
case LUA_TNUMBER: {
int k;
lua_Number n = nvalue(key);
lua_number2int(k, n);
if (luai_numeq(cast_num(k), n)) /* index is int? */
return luaH_getint(t, k); /* use specialized version */
/* else go through */
}
default: {
Node *n = mainposition(t, key);
do { /* check whether `key' is somewhere in the chain */
if (luaV_rawequalobj(gkey(n), key))
return gval(n); /* that's it */
else n = gnext(n);
} while (n);
return luaO_nilobject;
}
}
}
当key为LUA_TNUMBER时,调用luaH_getint
/*
** search function for integers
*/
const TValue *luaH_getint (Table *t, int key) {
/* (1 <= key && key <= t->sizearray) */
if (cast(unsigned int, key-1) < cast(unsigned int, t->sizearray))
return &t->array[key-1];
else {
lua_Number nk = cast_num(key);
Node *n = hashnum(t, nk);
do { /* check whether `key' is somewhere in the chain */
if (ttisnumber(gkey(n)) && luai_numeq(nvalue(gkey(n)), nk))
return gval(n); /* that's it */
else n = gnext(n);
} while (n);
return luaO_nilobject;
}
}
当key小于数组长度时,则直接返回数组中的值,否则计算key的hash值,从表的hash部分查找key的值。
当key为LUA_TSHRSTR时,调用luaH_getstr:
/*
** search function for short strings
*/
const TValue *luaH_getstr (Table *t, TString *key) {
Node *n = hashstr(t, key);
lua_assert(key->tsv.tt == LUA_TSHRSTR);
do { /* check whether `key' is somewhere in the chain */
if (ttisshrstring(gkey(n)) && eqshrstr(rawtsvalue(gkey(n)), key))
return gval(n); /* that's it */
else n = gnext(n);
} while (n);
return luaO_nilobject;
}
#define hashpow2(t,n) (gnode(t, lmod((n), sizenode(t))))
#define hashstr(t,str) hashpow2(t, (str)->tsv.hash)
当key为其他类型时,则统一调用mainposition获取其hash值对应的散列地址。我们来看看mainposition支持哪些类型。
static Node *mainposition (const Table *t, const TValue *key) {
switch (ttype(key)) {
case LUA_TNUMBER:
return hashnum(t, nvalue(key));
case LUA_TLNGSTR: {
TString *s = rawtsvalue(key);
if (s->tsv.extra == 0) { /* no hash? */
s->tsv.hash = luaS_hash(getstr(s), s->tsv.len, s->tsv.hash);
s->tsv.extra = 1; /* now it has its hash */
}
return hashstr(t, rawtsvalue(key));
}
case LUA_TSHRSTR:
return hashstr(t, rawtsvalue(key));
case LUA_TBOOLEAN:
return hashboolean(t, bvalue(key));
case LUA_TLIGHTUSERDATA:
return hashpointer(t, pvalue(key));
case LUA_TLCF:
return hashpointer(t, fvalue(key));
default:
return hashpointer(t, gcvalue(key));
}
}
表的key可以是lua中能表示的任意类型。
当给table的key赋值的时候,会先查找key是否存在,如果存在则对value重新赋值,如果不存在则表示key也不存在,会调用luaH_newkey创建key,然后再对value赋值。在创建key的时候如果table的大小不够会触发rehash对表进行扩大。
void luaV_settable (lua_State *L, const TValue *t, TValue *key, StkId val) {
int loop;
for (loop = 0; loop < MAXTAGLOOP; loop++) {
const TValue *tm;
if (ttistable(t)) { /* `t' is a table? */
Table *h = hvalue(t);
// 先判断key值是否存在
TValue *oldval = cast(TValue *, luaH_get(h, key));
/* if previous value is not nil, there must be a previous entry
in the table; moreover, a metamethod has no relevance */
if (!ttisnil(oldval) ||
/* previous value is nil; must check the metamethod */
((tm = fasttm(L, h->metatable, TM_NEWINDEX)) == NULL &&
/* no metamethod; is there a previous entry in the table? */
(oldval != luaO_nilobject ||
/* no previous entry; must create one. (The next test is
always true; we only need the assignment.) */
// key值不存在则创建key,如果table不够大了,就会扩大table,扩大table的时候需要对所有的key,value键对重新挪动位置
(oldval = luaH_newkey(L, h, key), 1)))) {
........
看下luaH_get调用的重新分配table大小的函数rehash
static void rehash (lua_State *L, Table *t, const TValue *ek) {
int nasize, na;
int nums[MAXBITS+1]; /* nums[i] = number of keys with 2^(i-1) < k <= 2^i */
int i;
int totaluse;
for (i=0; i<=MAXBITS; i++) nums[i] = 0; /* reset counts */
//计算数组部分的大小
nasize = numusearray(t, nums); /* count keys in array part */
totaluse = nasize; /* all those keys are integer keys */
//对hash部分进行遍历,计算hash部分中key为number的大小和不是number的大小
totaluse += numusehash(t, nums, &nasize); /* count keys in hash part */
/* count extra key */
//加上要创建的key,如果key为number,则nasize加1
nasize += countint(ek, nums);
totaluse++;
/* compute new size for array part */
//根据当前key的统计,重新计算数组部分的大小,结果保存到na中
na = computesizes(nums, &nasize);
/* resize the table to new computed sizes */
luaH_resize(L, t, nasize, totaluse - na);
}
来看看怎样重新计算数组部分大小的:
static int computesizes (int nums[], int *narray) {
int i;
int twotoi; /* 2^i */
int a = 0; /* number of elements smaller than 2^i */
int na = 0; /* number of elements to go to array part */
int n = 0; /* optimal size for array part */
for (i = 0, twotoi = 1; twotoi/2 < *narray; i++, twotoi *= 2) {
if (nums[i] > 0) {
a += nums[i];
if (a > twotoi/2) { /* more than half elements present? */
n = twotoi; /* optimal size (till now) */
na = a; /* all elements smaller than n will go to array part */
}
}
if (a == *narray) break; /* all elements already counted */
}
*narray = n;
lua_assert(*narray/2 <= na && na <= *narray);
return na;
}
数组nums是按块保存个数的,2^(i-1) < k <= 2^i (i=1,2,3,..., 31)的属于同一块,比如key=1,则key放在nums[0]的块,key=2则放在nums[1]的块,key=3,4则存放在nums[2]的块,k=5, 8存放在nums[3]的块,如此类推。分配数组大小的规则是:遍历nums的所有块,所有块的已被使用的key的总和(对应a)大于当前块所能存放的key的上限的一半(对应twotoi/2),则数组大小取当前key的上限(对应twotoi)。可以认为a是key的密集程度,如果所有key都使用了,则密集程度为1,如果没有key被使用,则密集程度为0,computessizes就是求出key的密集程度大于0.5小于1的情况。
以上就是对lua中TString和Table数据结构的分析。记录下来供自己查看。其他的结构以后继续分析