程序员程序猿阵线联盟-汇总各类技术干货

Lua字符串的设计

2018-05-30  本文已影响9人  沧州宁少

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在设计之初就把性能和资源占用放到重要位置。

字符串的数据结构如下

 typedef union TString {
   L_Umaxalign dummy;  /* ensures maximum alignment for strings */
   struct {
     CommonHeader;
     lu_byte reserved;
     unsigned int hash;
     size_t len;
   } tsv;
 } TString;

关于字符串的数据结构具体几部分上一篇有解释,现在重复一遍。

我们刚才提到了会有一个散列桶会存储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点。

下面看下如果查找不到创建一个新串的过程

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;
}

总结上面

具体的重散列的过程如下

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;
}

重散列流程如下(上面代码掺杂防御式编程的思想,进行具体的流程之前,对当前的状态及传入的数据进行异常判断)

上一篇 下一篇

猜你喜欢

热点阅读