Lua字符串的设计

Lua字符串的设计

在Lua中字符串是一种被内化的数据类型。怎么理解被内化的含义呢?简单来说就是每个存放Lua字符串的变量。存储的都不是字符串内容的拷贝,而是它的一份引用。因此每当创建新的字符串的时候,我们首先要检查系统中是否已经存在一份相同的字符串了,如果有的话,直接让当前的指针变量指向它,否则就创建一份新的字符串数据,然后让当前指针指向。

因为字符串并非保存的是实际内容的拷贝而是引用,因此Lua字符串是不可变的。我们联想下Java或者OC中字符串不可变的原因。是否也是相同的设计,保存的只是字符串的引用,因为一旦改变了字符串a的引用,a原来指向的内存中的内容并不会改变。Java和OC都推荐直接使用字面量的方式创建字符串,都说这样高效。那高效体现在什么地方呢。

Java 字符串通过字面量创建首先会去字符串池去查找有无相应的字符串,如果有则直接让当前变量执行相应的内存。同样OC字面量创建会自动去常量存储区去查找,也是类似的机制。那么我们是否可以推断3种语言的字符串底层设计是一样的?

a = '1'
a = '1'..'2'

上面代码具体的执行过程如下图

《Lua字符串的设计》 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;

《Lua字符串的设计》 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起来
  • 释放旧的散列桶,赋值新的。
    原文作者:沧州宁少
    原文地址: https://www.jianshu.com/p/943d519a2f29
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞