Lua字符串的设计

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起来
  • 释放旧的散列桶,赋值新的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容