Lua字符串的设计
在Lua中字符串是一种被内化的数据类型。怎么理解被内化的含义呢?简单来说就是每个存放Lua字符串的变量。存储的都不是字符串内容的拷贝,而是它的一份引用。因此每当创建新的字符串的时候,我们首先要检查系统中是否已经存在一份相同的字符串了,如果有的话,直接让当前的指针变量指向它,否则就创建一份新的字符串数据,然后让当前指针指向。
因为字符串并非保存的是实际内容的拷贝而是引用,因此Lua字符串是不可变的。我们联想下Java或者OC中字符串不可变的原因。是否也是相同的设计,保存的只是字符串的引用,因为一旦改变了字符串a的引用,a原来指向的内存中的内容并不会改变。Java和OC都推荐直接使用字面量的方式创建字符串,都说这样高效。那高效体现在什么地方呢。
Java 字符串通过字面量创建首先会去字符串池去查找有无相应的字符串,如果有则直接让当前变量执行相应的内存。同样OC字面量创建会自动去常量存储区去查找,也是类似的机制。那么我们是否可以推断3种语言的字符串底层设计是一样的?
a = '1'
a = '1'..'2'
上面代码具体的执行过程如下图
image-20180530153925382.png改变a的只是a的指向,并不会对a原先指向的内存产生任何影响。
上面谈到了Lua字符串是一种被内化的数据类型。那么为了实现Lua字符串创建之前的查询,必须有一个空间去存储已经创建而且没有被GC的所有字符串。Lua虚拟机使用一个散列桶的方式管理字符串。
Lua在字符串实现上使用内化的优点在于。
- 时间上。进行字符串的查找和比较操作的时候性能会有很大的提高。传统的字符串的查找和比较,需要遍历字符串的每一个字符,时间复杂度取决于字符串的长度,是线性递增的。而Lua进行字符串的比较和查询的时候,首先去比较字符串的hash值,这一步是O(1)的时间复杂度,这一步没有比较出来后,会对具体的两个字符串进行长度比较,最后仍然没有比较出来,才会进行逐位比较。大多情况下,前2步会对绝大多数情况进行过滤,字符串比较和查询很大程度是O(1)的时间复杂度。
- 空间上。因为是一个内化的方式,所有相关的字符串是共享一块内存。这极大的节省了相同字符串带来的内存消耗。
以上设计可以看到,Lua在设计之初就把性能和资源占用放到重要位置。
字符串的数据结构如下
typedef union TString {
L_Umaxalign dummy; /* ensures maximum alignment for strings */
struct {
CommonHeader;
lu_byte reserved;
unsigned int hash;
size_t len;
} tsv;
} TString;
关于字符串的数据结构具体几部分上一篇有解释,现在重复一遍。
- L_Umaxalign 是一个联合体define LUAI_USER_ALIGNMENT_T union { double u; void *s; long l; } 它的类型大小会取这三种类型中最大的。在C语言中,struct/unicon会按照最大字节进行内存对齐的,为的降低CPU获取数据的访问周期,提高CPU的访问效率。
- 内部的结构体tsv。CommonHeader同理,
- reserved标注当前字符串是否是关键字,标记的关键字将不会在GC阶段被回收
- hash 改字符串的散列值。Lua字符串比较并不会像其他的语言那种一般的做法进行诸位比较。而是仅仅比较字符串的散列值。
- len 字符串的长度
我们刚才提到了会有一个散列桶会存储Lua中所有的字符串。这个变量是global_state 的strt。具体的结构
typedef struct stringtable {
GCObject **hash; // 存放的GCObject *的list
lu_int32 nuse; /* number of elements */
int size; // 散列桶的大小
} stringtable;
image-20180530160203156.png
散列桶的结构类似上图。外层数组,内层链表的结构。
接下来我们看一下创建一个新的字符串的过程
伪代码
根据字符串的hash值去散列桶去查找具体的桶
找到对应的桶后,先比较长度
长度相同,逐位比较字符
均相同找到字符串,看当前字符串处于的状态,if isdead(o) 则改变字符串状态并返回
没找到直接创建新字符串
真实代码
TString *luaS_newlstr (lua_State *L, const char *str, size_t l) {
GCObject *o;
// 类型转换
unsigned int h = cast(unsigned int, l); /* seed */
//右移运算。 比如32 二进制表示 00100000 则右面移5位 为00000001 = 2
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);
// 先比较长度,长度不符合继续查找next,长度符合诸位比较
if (ts->tsv.len == l && (memcmp(str, getstr(ts), l) == 0)) {
/* string may be dead */
if (isdead(G(L), o)) changewhite(o);
return ts;
}
}
// 如果找不到对应的散列桶直接去创建新的字符串。
return newlstr(L, str, l, h); /* not found */
}
上面具体的过程注意的地方2点。
- 当字符串特别大的时候没有必要诸位比较,每次比较取一个步长
- 获取的当前字符串如果处于GC阶段,改变它的状态,达到复用的目的。
下面看下如果查找不到创建一个新串的过程
static TString *newlstr (lua_State *L, const char *str, size_t l,
unsigned int h) {
// 一个新的字符串的指针
TString *ts;
// stringable 结构体指针
stringtable *tb;
// 如果字符串过大,直接报错返回NULL
if (l+1 > (MAX_SIZET - sizeof(TString))/sizeof(char))
luaM_toobig(L);
// 创建字符串
ts = cast(TString *, luaM_malloc(L, (l+1)*sizeof(char)+sizeof(TString)));
// 字符串的长度为l
ts->tsv.len = l;
// 字符串的散列值为h
ts->tsv.hash = h;
// 字符串为白色(标记GC的时候回根据字符串的状态进行回收)
ts->tsv.marked = luaC_white(G(L));
// 数据类型
ts->tsv.tt = LUA_TSTRING;
// 不是关键字
ts->tsv.reserved = 0;
// 拷贝str到ts+1位置
memcpy(ts+1, str, l*sizeof(char));
// 字符串最后一位'\0'
((char *)(ts+1))[l] = '\0'; /* ending 0 */
// 获取global_State中的散列桶
tb = &G(L)->strt;
// 计算散列通中的存储位置
h = lmod(h, tb->size);
// 如果原来位置有元素则next指针指向它。
ts->tsv.next = tb->hash[h]; /* chain new entry */
// 类型转换TString转为GCObject
tb->hash[h] = obj2gco(ts);
// 数量+1
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上每个Value的值。
- 计算当前字符串在散列桶上对应的桶,如果有值,则当前字符串的next指针指向对应桶(链表)的首位
- 将对应的桶的首位赋值为创建的TString
- 元素数量+1
- 如果每个桶上元素过多,触发重散列。
具体的重散列的过程如下
void luaS_resize (lua_State *L, int newsize) {
// GCObject * 的列表
GCObject **newhash;
// 结构体指针
stringtable *tb;
int I;
// GC阶段则返回
if (G(L)->gcstate == GCSsweepstring)
return; /* cannot resize during GC traverse */
// 创建一个List
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];
// p为桶中的header指针
while (p) { /* for each node in the list */
GCObject *next = p->gch.next; /* save next */
// 获取对应的TString的hash值
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指针指向当前位置。为了是p->gch成为header
p->gch.next = newhash[h1]; /* chain it */
// 成为header
newhash[h1] = p;
// 赋值循环取链表下一个节点
p = next;
}
}
//释放旧的散列桶
luaM_freearray(L, tb->hash, tb->size, TString *);
tb->size = newsize;
tb->hash = newhash;
}
重散列流程如下(上面代码掺杂防御式编程的思想,进行具体的流程之前,对当前的状态及传入的数据进行异常判断)
- 判断当前状态,GC阶段则Return
- 开辟并初始化新的GCObject **
- 遍历原来的散列桶。遍历每个具体的桶,根据hash算法拿到的新的散列值确定当前元素的位置。并chain起来
- 释放旧的散列桶,赋值新的。
网友评论