lua设计与实现(三)字符串

概述

  • 在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中,应该尽量少地使用字符串连接操作符,因为每次都会生成个新的字符串。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis是一个开源的内存数据库管理系统,根据键值存储的方式进行数据存储和读取。它的设计实现相当巧妙。 首先,Redis以C语言编写,无依赖,内存占用低,延迟低,处理高并发请求的能力强。它的设计使得它可以快速处理各种读写操作,非常适合用作缓存服务器。 其次,Redis的核心数据结构是基于键值对的,主要有字符串、列表、集合、有序集合和哈希表。这些数据结构设计简洁高效,支持丰富的操作,可以满足不同场景的需求。例如,列表支持从两端插入和提取元素的操作,集合支持交集、并集和差集的操作,而有序集合则可以进行排名和范围查询等。 此外,Redis还具备持久化和复制功能,保证数据的安全性和可用性。它提供了两种持久化方式:RDB快照和AOF日志。RDB快照可以将数据库状态保存到硬盘上,以便在Redis重启时进行恢复;AOF日志则以追加的方式记录每次写操作,以保证数据的完整性。而复制功能使得可以将数据复制到多台服务器上,以提供更高的可用性和负载均衡。 最后,Redis还提供了丰富的扩展功能,如发布/订阅模式、事务、Lua脚本、管道和集群等。这些功能的设计实现都遵循了简洁、高效、易用的原则,为用户提供了更多的选择和灵活性。 总之,Redis的设计实现非常出色,它以高效的内存读写、多种数据结构的支持、持久化和复制功能、丰富的扩展性等优势,成为了一个非常受欢迎和广泛使用的内存数据库管理系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值