lua源码分析之string类型的实现

Posted 太合音乐技术团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lua源码分析之string类型的实现相关的知识,希望对你有一定的参考价值。

导语

lua作为一门特别轻量级的脚本开发语言,其开发效率与运行效率均已得到业内各大公司的广泛认可。其与nginx结合的openresty也是目前百度音乐API模块以及基础服务模块的重要开发工具之一。lua中的基础数据类型一共分为8种:

  1. nil

  2. boolean

  3. number

  4. function

  5. userdata

  6. table

  7. thread

  8. string

而本文将围绕lua的string类型,即字符串类型的C语言实现,分析lua是如何完成string类型的数据结构封装以及string类型的一些常用方法实现。


lua string的数据结构实现

lua的string类型的数据结构随着lua的版本更新也发生过比较大程度的变化。从lua 5.2.1起,string类型在lua中以两类形式分别进行存储:短字符串与长字符串。而如此设计的原因将在下面进行详细描述。以下所有的分析均基于lua 5.2.1版本源码。

字符串类型TString的定义如下:

/*
** Header for string value; string bytes follow the end of this structure
*/

typedef union TString {
 L_Umaxalign dummy;  /* ensures maximum alignment for strings */
 struct {
   CommonHeader;
   lu_byte extra;  /* reserved words for short strings; "has hash" for longs */
   unsigned int hash;
   size_t len;  /* number of characters in string */
 } tsv;
} TString;

(注:lobject.h 410-421行 string类型定义

可以看出,Tstring类型是一个联合体。L_Umaxalign类型变量dummy是用于字符串的字节对齐的,用于提升字符串的查找效率。struct中主要包含以下4项:

  1. CommonHeader:统一适配于lua中的gc机制

  2. extra:辅助信息字段,主要为短字符串类型提供是否该字符串为lua保留关键字使用

  3. hash:字符串hash值,用于加速字符串的查找匹配

  4. len:字符串长度

lua新生成一个string的方法为:

/*
** new string (with explicit length)
*/

TString *luaS_newlstr (lua_State *L, const char *str, size_t l) {
 if (l <= LUAI_MAXSHORTLEN)  /* short string? */
   return internshrstr(L, str, l);
 else {
   if (l + 1 > (MAX_SIZET - sizeof(TString))/sizeof(char))
     luaM_toobig(L);
   return createstrobj(L, str, l, LUA_TLNGSTR, G(L)->seed, NULL);
 }
}

(注:lstring.c 153-164行 lua生成字符串

从代码中可以清楚的看出,lua新生成一个字符串会根据字符串长度来建立短字符串或长字符串,判断规则基于字符串长度与LUAI_MAXSHORTLEN的比较值。在lua源码中,LUAI_MAXSHORTLEN的默认值设置为40字节。且对于短字符串而言lua做了唯一化处理,相同的短字符串在lua State内仅存放一份,而长字符串则不会进行唯一化处理。

在lua 5.2.0之前,字符串无论长短均以短字符串的形式进行存储的。为何lua 5.2.1之后需要将长短字符串分开处理呢,首先我们需要了解一下lua在短字符串上是如何进行存储的。

lua的短字符串是存放在一个全局global_State的hash桶string table中的。其代码实现如下:

typedef struct stringtable {
 GCObject **hash;
 lu_int32 nuse;  /* number of elements */
 int size;
} stringtable;

(注:lstate.h 59-63行 字符串hash桶实现

每次在新创建一个string类型的数据时,首先计算出这个string类型的hash值,并在hash桶中以链表的形式进行存放。如下图所示:

(注:图片参考自codedump大神的《lua设计与实现》第19页)

过程可以简单总结为:先hash,后线性。在老版本中(以lua 5.1.4为例)寻找字符串hash的过程如下:

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]));

(注:lua 5.1.4 lstring.c 77-81行 字符串hash过程

对于长字符串而言,由于计算hash值是跳跃计算的,那么以上算法很可能会导致许多的长字符串会计算出相同的hash,进而可能在string table中的某一hash上会产生很大程度的冗余,严重影响了字符串的查找速度。因此lua 5.2.1之后将长字符串独立出来,仅仅将长度较短的字符串放在string table中进行查找,以加速字符串的查找速度。

长字符串在创建时并不会计算hash值,仅仅对extra值进行简单置0,在第一次查询的时候会进行一次计算,以加速后续长字符串的查找速度。长字符串计算hash值的代码如下:

TString *s = rawtsvalue(key);
if (s->tsv.extra == 0) {  /* no hash? */
s->tsv.hash = luaS_hash(getstr(s), s->tsv.len, s->tsv.hash);
s->tsv.extra = 1;  /* now it has its hash */
}
return hashstr(t, rawtsvalue(key));

(注:ltable.c 102-107行 长字符串hash过程

继续回到上面string table的问题,当存放的字符串过多时,string table的hash会因单个GCObject *下的链表长度过长,退化为线性查找的过程,继而查找字符串的速度依旧很慢。对于这一点,lua采用了resize的方式重新分配一个新的hash桶来存放所有的字符串。hash桶resize的过程如下:

/*
** resizes the string table
*/

void luaS_resize (lua_State *L, int newsize) {
 int i;
 stringtable *tb = &G(L)->strt;
 /* cannot resize while GC is traversing strings */
 luaC_runtilstate(L, ~bitmask(GCSsweepstring));
 if (newsize > tb->size) {
   luaM_reallocvector(L, tb->hash, tb->size, newsize, GCObject *);
   for (i = tb->size; i < newsize; i++) tb->hash[i] = NULL;
 }
 /* rehash */
 for (i=0; i<tb->size; i++) {
   GCObject *p = tb->hash[i];
   tb->hash[i] = NULL;
   while (p) {  /* for each node in the list */
     GCObject *next = gch(p)->next;  /* save next */
     unsigned int h = lmod(gco2ts(p)->hash, newsize);  /* new position */
     gch(p)->next = tb->hash[h];  /* chain it */
     tb->hash[h] = p;
     resetoldbit(p);  /* see MOVE OLD rule */
     p = next;
   }
 }
 if (newsize < tb->size) {
   /* shrinking slice must be empty */
   lua_assert(tb->hash[newsize] == NULL && tb->hash[tb->size - 1] == NULL);
   luaM_reallocvector(L, tb->hash, tb->size, newsize, GCObject *);
 }
 tb->size = newsize;
}

(注:lstring.c 61-92行 hashresize过程

总结来说,hash桶resize的过程一共分为3步:

  1. 申请一个新的hash桶,并将新的hash桶中的值全部置为NULL;

  2. 遍历现在内存中存放的hash桶,并将原先的数据导入到新的hash桶中;

  3. 释放之前的hash桶,并将新的hash桶设置为当前使用的hash桶。

以上是对lua的string类型数据结构的简单代码分析,接下来对string类型的操作方法进行简单介绍。主要介绍string类型的比较以及string类型的拼接。

 

lua string的操作方法

字符串比较

字符串类型的比较方法代码如下所示:

/*
** equality for strings
*/

int luaS_eqstr (TString *a, TString *b) {
 return (a->tsv.tt == b->tsv.tt) &&
        (a->tsv.tt == LUA_TSHRSTR ? eqshrstr(a, b) : luaS_eqlngstr(a, b));
}

(注:lstring.c 42-48行 字符串类型比较方法

比较过程如下:

  1. 首先比较字符串类型,若两个字符串类型不相同,即不均为短字符串或长字符串,则两个字符串必然不相等。

  2. 若字符串类型相同,则分别对于两个字符串都是短字符串或两个字符串都是长字符串进行比较。若两个字符串都是短字符串,其比较方法如下:

/*
** equality for short strings, which are always internalized
*/
#define eqshrstr(a,b)    check_exp((a)->tsv.tt == LUA_TSHRSTR, (a) == (b))

(注:lstring.h 31-34行 短字符串比较方法

若两个字符串都是长字符串,其比较方法如下:

/*
** equality for long strings
*/

int luaS_eqlngstr (TString *a, TString *b) {
 size_t len = a->tsv.len;
 lua_assert(a->tsv.tt == LUA_TLNGSTR && b->tsv.tt == LUA_TLNGSTR);
 return (a == b) ||  /* same instance or... */
   ((len == b->tsv.len) &&  /* equal length and ... */
    (memcmp(getstr(a), getstr(b), len) == 0));  /* equal contents */
}

(注:lstring.c 30-39 长字符串比较方法

首先比较字符串长度,若字符串长度不同,则两个字符串必然不相等。若字符串长度相等,则需逐字节对字符串进行比较以判定两个长字符串是否相等。


字符串拼接

由于lua的string结构设计,可以看出lua在做字符串类型拼接的时候,每一次新的拼接都会新生成一个字符串,进而每一次这样的操作都需新申请一块内存空间来存放新的字符串,而不会去替换老的字符串。那么存在一个问题,当字符串拼接操作过多的情况下,是否会导致拼接过程非常的慢呢,以下面的例子为例,使用字符串拼接方法拼接得到一个长度为500000的字符串,其lua代码如下:

start_time = os.clock()
local str = ''
for i = 1, 500000 do
   str = str .. 'a'
end
end_time = os.clock()
print(end_time - start_time)

在笔者的操作系统中,这段代码的执行时间为:30.837541秒。可以看到,大量使用字符串拼接方法会导致代码执行异常缓慢,原因是因为每次都需要从内存中获取到字符串的实际数值,并申请存放新的字符串的内存空间,整个过程的时间基本全部消耗在了内存的数据读取与存储上。

那么是否有方法能够快速完成这种长度很大的字符串拼接呢,答案是有的。其lua代码如下:

start_time = os.clock()
local str = ''
local t = {}
for i = 1, 500000 do
   t[#t + 1] = 'a'
end
str = table.concat(t, '')
end_time = os.clock()
print(end_time - start_time)

在笔者的操作系统中,这段代码的执行时间为:0.114616秒。通过使用lua的table临时存放需要拼接的字符串元素,通过table.concat完成字符串的拼接,这种操作仅仅只会申请一次字符串的内存空间,进而大幅度减少了内存的反复读取与存储。

通过以上示例可以看出,在大量的需要执行字符串拼接操作时,通过table.concat替代简单的字符串拼接方法,能更高效的完成这一操作。


结束语

lua的string类型作为lua的最重要的数据类型之一,其实现利用了短字符串的唯一化以及string table加速了字符串的查找与比较过程,但是同时也带来了字符串拼接时可能会导致大量的内存读取与存储的过程。在使用lua这一门语言进行开发时,需尽可能的利用其语言实现特性,加速代码的执行速度,同时避免lua在语言设计上的一些问题,使用其他方法提升代码的执行效率。

 

参考资料

  1. codedump. Lua设计与实现. 北京:人民邮电出版社,2017-8.

  2. 云风. Lua源码赏析.pdf.


以上是关于lua源码分析之string类型的实现的主要内容,如果未能解决你的问题,请参考以下文章

lua 源码分析之线程对象lua_State

JDK源码阅读之 HashMap

全网最硬核的源码分析之——String源码分析

lua源码笔记-基本数据结构

UE4 Unlua源码解析7 - Lua通过UE命名空间访问C++类型的实现原理

UE4 Unlua源码解析7 - Lua通过UE命名空间访问C++类型的实现原理