概述
- 在Lua虚拟机中存在一个全局的数据区(散列桶),用来存放当前系统中的所有字符串。
- 同一个字符串在Lua虚拟机中只可能有一个副本,一个字符串一单创建,将是不可变更的。
- 变量存放的仅是字符串的引用,而不是其内容
Lua字符串内化的优点:
传统字符串的比较与查找是根据字符串长度逐位比较,时间复杂度与字符串长度线性相关。而Lua的,在已知字符串散列值的情况下,只需要一次整数比较。
多份相同的字符串在整个系统中只存在一份副本。
缺点:
以前面描述的创建字符串的过程来说,在创建一个新的字符串时,首先会检查系统中是否有相同的数据,只有不存在的情况下才创建,这与直接创建字符串相比,多了一次查找过程好在在Lua的实现中,查找一个字符串的操作消耗并不算大。
字符串实现
字符串的数据结构定义:
//lobject.h
/*
** String headers for string table
*/
typedef union TString {
L_Umaxalign dummy; /* ensures maximum alignment for strings */
struct {
CommonHeader;
lu_byte reserved;
unsigned int hash;
size_t len;
} tsv;
} TString;
可以看到这是一个联合体,其目的是为了让TString数据类型按照L_Umaxalign 类型的大小对齐
//llimits.h
/* type to ensure maximum alignment */
typedef LUAI_USER_ALIGNMENT_T L_Umaxalign;
//luaconf.h
/*
@@ LUAI_USER_ALIGNMENT_T is a type that requires maximum alignment.
** CHANGE it if your system requires alignments larger than double. (For
** instance, if your system supports long doubles and they must be
** aligned in 16-byte boundaries, then you should add long double in the
** union.) Probably you do not need to change this.
*/
#define LUAI_USER_ALIGNMENT_T union { double u; void *s; long l; }
在c语言中struct/union这样的复合数据类型是按照这个类型中最大的数据来对齐的,所以这里就按照double来对齐,一般是8字节。之所以要进行对齐操作,是为了cpu读取数据时性能更高。
TString其余变量的含义:
- CommonHeader:前面通用数据结构已解释,用于垃圾回收的对象共有的。
- reserved:这个变量用于标识字符串是否是Lua虚拟机中的保留字符串,如果这个值不为0,那么将不会在GC阶段被回收,而是一直保留在系统中。只有Lua语言的关键之才是保留字符。
- hash:该字符串的散列值。前面提到过,lua的字符串比较并不会像一般的做法那样进行逐位比较,而是比较散列值。
- len:字符串长度。
lua存放字符串的全局变量就是global_state的strt成员,这是一个散列数组,专门用于存放字符串:
//lstate.h
typedef struct stringtable {
GCObject **hash;
lu_int32 nuse; /* number of elements */
int size;
} stringtable;
当新建一个字符串元素TString时,首先计算出字符串的散列值,这就是散列数组的索引,如果这里已有元素,则使用链表串接起来。如图
使用散列桶存放数据,有一个问题需要考虑,那就是当数据量非常大的时候,分配到每个桶上的数据也会非常多,那么一次查找又会退化为线性查找。所以需要一个重新散列(rehash)的过程,这就是当字符串非常多的时候,重新分配桶的数量,降低分配到每个桶的数据数量,这个过程在函数luaS_resize中。
//lstring.c
void luaS_resize (lua_State *L, int newsize) {
GCObject **newhash;
stringtable *tb;
int i;
if (G(L)->gcstate == GCSsweepstring)
return; /* cannot resize during GC traverse */
newhash = luaM_newvector(L, newsize, GCObject *);
tb = &G(L)->strt;
for (i=0; i<newsize; i++) newhash[i] = NULL;
/* rehash */
for (i=0; i<tb->size; i++) {
GCObject *p = tb->hash[i];
while (p) { /* for each node in the list */
GCObject *next = p->gch.next; /* save next */
unsigned int h = gco2ts(p)->hash;
int h1 = lmod(h, newsize); /* new position */
lua_assert(cast_int(h%newsize) == lmod(h, newsize));
p->gch.next = newhash[h1]; /* chain it */
newhash[h1] = p;
p = next;
}
}
luaM_freearray(L, tb->hash, tb->size, TString *);
tb->size = newsize;
tb->hash = newhash;
}
GCSsweepstring:当前GC处在回收字符串数据阶段。
触发这个resize操作的地方有两个:
- lgc.c 的checkSizes函数:这里会进行检查,如果此时桶的数量太大,比如是实际存放的字符串数量的4倍,那么会将散列桶数组减少为原来的一半.
- lstrng.c的newlstr函数:如果此时字符串的数量大于桶数组的数量,且桶数组的数量小MAXINT/2,那么就进行翻倍的扩容。
分配一个新的字符串,代码在luaS_newlstr:
//lstring.c
static TString *newlstr (lua_State *L, const char *str, size_t l,
unsigned int h) {
TString *ts;
stringtable *tb;
if (l+1 > (MAX_SIZET - sizeof(TString))/sizeof(char))
luaM_toobig(L);
ts = cast(TString *, luaM_malloc(L, (l+1)*sizeof(char)+sizeof(TString)));
ts->tsv.len = l;
ts->tsv.hash = h;
ts->tsv.marked = luaC_white(G(L));
ts->tsv.tt = LUA_TSTRING;
ts->tsv.reserved = 0;
memcpy(ts+1, str, l*sizeof(char));
((char *)(ts+1))[l] = '\0'; /* ending 0 */
tb = &G(L)->strt;
h = lmod(h, tb->size);
ts->tsv.next = tb->hash[h]; /* chain new entry */
tb->hash[h] = obj2gco(ts);
tb->nuse++;
if (tb->nuse > cast(lu_int32, tb->size) && tb->size <= MAX_INT/2)
luaS_resize(L, tb->size*2); /* too crowded */
return ts;
}
TString *luaS_newlstr (lua_State *L, const char *str, size_t l) {
GCObject *o;
unsigned int h = cast(unsigned int, l); /* seed */
size_t step = (l>>5)+1; /* if string is too long, don't hash all its chars */
size_t l1;
for (l1=l; l1>=step; l1-=step) /* compute hash */
h = h ^ ((h<<5)+(h>>2)+cast(unsigned char, str[l1-1]));
for (o = G(L)->strt.hash[lmod(h, G(L)->strt.size)];
o != NULL;
o = o->gch.next) {
TString *ts = rawgco2ts(o);
if (ts->tsv.len == l && (memcmp(str, getstr(ts), l) == 0)) {
/* string may be dead 判断这个字符串在当前GC夹断被判定为回收,如果是,则修改为不需要进行回收。*/
if (isdead(G(L), o)) changewhite(o);
return ts;
}
}
return newlstr(L, str, l, h); /* not found */
}
(1)计算需要新创建的字符串对应的散列值。计算步长是为了 字符串非常大的时候不需要逐位来算,仅需要每个步长取一个字符就可以了
(2)根据散列值找到对应的散列桶,遍历该散列桶的所有元素,如果能够查找到同样的字符串,说明之前已经存在相同字符串,此时不需要重新分配一个新的字符串数据,直接返回即可
(3)如果第(2)步中查找不到相同的字符串,调用newlstr函数建一个新的字符串。
最后,reserved字段,用于标识是不是保留字符串。lua中的关键字都是保留字符串,最开始赋值:
//llex.c
void luaX_init (lua_State *L) {
int i;
for (i=0; i<NUM_RESERVED; i++) {
TString *ts = luaS_new(L, luaX_tokens[i]);
luaS_fix(ts); /* reserved words are never collected */
lua_assert(strlen(luaX_tokens[i])+1 <= TOKEN_LEN);
ts->tsv.reserved = cast_byte(i+1); /* reserved word */
}
}
这里reserved存放的是数组luaX_tokens的索引,这样,一方面可以快速定位到哪个关键字,另一方面,如果不为0,则是保留字符串。
//llex.c
/* ORDER RESERVED */
const char *const luaX_tokens [] = {
"and", "break", "do", "else", "elseif",
"end", "false", "for", "function", "if",
"in", "local", "nil", "not", "or", "repeat",
"return", "then", "true", "until", "while",
"..", "...", "==", ">=", "<=", "~=",
"<number>", "<name>", "<string>", "<eof>",
NULL
};
这里的每个字符串都与某个保留字Token类型一一对应:
//llex.h
/*
* WARNING: if you change the order of this enumeration,
* grep "ORDER RESERVED"
*/
enum RESERVED {
/* terminal symbols denoted by reserved words */
TK_AND = FIRST_RESERVED, TK_BREAK,
TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION,
TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, TK_REPEAT,
TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE,
/* other terminal symbols */
TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE, TK_NUMBER,
TK_NAME, TK_STRING, TK_EOS
};
需要说明的是,上面luaX_tokens字符串数组中的气number, name, string, eof,
这几个字符串并不真实作为保留关键字存在,但是因为有相应的保留字Token类型,所以也就干脆这么定义个对应的字符串了。
有了以上认知,不难理解在Lua中,应该尽量少地使用字符串连接操作符,因为每次都会生成个新的字符串。