热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:Lua中table长度到底是怎么来的

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Lua中table长度到底是怎么来的相关的知识,希望对你有一定的参考价值。 节前上班时同事在工作群里发了一张图,并感叹lua的table的长度真是诡异

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Lua中table长度到底是怎么来的相关的知识,希望对你有一定的参考价值。


节前上班时同事在工作群里发了一张图,并感叹lua的table的长度真是诡异。


↑乍一看似乎莫名其妙的长度

乍一看归结为不从1开始并顺序[k] = v 形式插入表的都已hash表形式存储,那么table的长度则不可预估这样简单粗暴的下了定论。


后来等工作空闲下来的时候,和另一位同事仔细过了一遍lua的源码和解析,终于明白事情并非那么简单,于是便有了今天这篇文章总结一下。




table的数据结构



































/*** Tables*/
typedef union TKey { struct { TValuefields; struct Node *next; /* for chaining */ } nk; TValue tvk;} TKey;

typedef struct Node { TValue i_val; TKey i_key;} Node;

typedef struct Table { CommonHeader; lu_byte flags; /* 1<

lu_byte lsizenode; /* log2 of size of `node' array */ struct Table *metatable; TValue *array; /* array part */ Node *node; Node *lastfree; /* any free position is before this position */ GCObject *gclist; int sizearray; /* size of `array' array */} Table;

由这部分源码我们可以知道table的数据是由两部分组成:



  • array与sizearray,数组部分与数组部分的长度


  • node与lsizenode,哈希部分(表头)与以2为底的哈希表长度的对数,即2lsizenode为哈希表长度






table的#如何取得的


首先我们找到#取表长度的源码进行分析


















































/*** Try to find a boundary in table `t'. A `boundary' is an integer index** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil).*/int luaH_getn (Table *t) { unsigned int j = t->sizearray; if (j > 0 && ttisnil(&t->array[j - 1])) { /* there is a boundary in the array part: (binary) search for it */ unsigned int i = 0; while (j - i > 1) { unsigned int m = (i+j)/2; if (ttisnil(&t->array[m - 1])) j = m; else i = m; } return i; } /* else must find a boundary in hash part */ else if (t->node == dummynode) /* hash part is empty? */ return j; /* that is easy... */ else return unbound_search(t, j);}
static int unbound_search (Table *t, unsigned int j) { unsigned int i = j; /* i is zero or a present index */ j++; /* find `i' and `j' such that i is present and j is not */ while (!ttisnil(luaH_getnum(t, j))) { i = j; j *= 2; if (j > cast(unsigned int, MAX_INT)) { /* overflow? */ /* table was built with bad purposes: resort to linear search */ i = 1; while (!ttisnil(luaH_getnum(t, i))) i++; return i - 1; } } /* now do a binary search between them */ while (j - i > 1) { unsigned int m = (i+j)/2; if (ttisnil(luaH_getnum(t, m))) j = m; else i = m; } return i;}


阅读源码结合注释,我们可以明白取长度时函数会在数组部分不为空且最后一个元素不为nil时尝试在数组部分进行二分法查找一个index,这个index需要满足t[index]不为nil而t[index+1]为nil的。


函数的三个分支分别为



  • table的数组长度大于0且最后一个取出表数组长度的的元素为nil


  • table的哈希部分为空(那么数组长度就是表长度)


  • 其他情况(进入下面的 unbound_search 来在哈希部分查找长度)



在对哈希部分查找长度时,进行边界扩大搜索,每次扩大为之前的二倍,再使用二分法进行查找number与nil的分界元素。


此时我们可以做一个小小的验证




























local t1 = {1,2,3,4,5}print("t1的长度为:",#t1)t1[7] = 7print("t1的长度为:",#t1)t1[9] = 9print("t1的长度为:",#t1)
print("---------------------")
local t2 = {1,2,3,4,5}print("t2的长度为:",#t2)t2[8] = 8print("t2的长度为:",#t2)t2[9] = 9print("t2的长度为:",#t2)
t1的长度为:5t1的长度为:5t1的长度为:5---------------------t2的长度为:5t2的长度为:8t2的长度为:9


我们可以看到t1在luaH_getn 的第一次打印中,sizearray 为8,但t[8]为nil,此时进入第一个分支进行二分法查找最终返回5,在第二次打印中依然t[8]为nil,此时依然返回5,第三次打印同理,一直在第一个逻辑分支中。


而t2在第一次打印中同上,第二次打印时,t[8]有值了,此时满足第二个分支即哈希表部分为空,表长度等于sizearray 为8。第三次打印sizearray 依然为8,进入第三个分支,即此时进入unbound_search 来查找表长度。此时先找到t2[9]不为nil,将j *= 2 翻倍为18,于是在9-18范围进行二分查找t[index]不为nil但t[index + 1]为nil的值,最终返回t[9]。


对于二分查找法虽然效率很高,但会存在不精确的问题。在二分临界值附近会出现极大的区别,也就是我在文章开头提到的情况。



















local a = {}for i = 5,12 do a[i] = i endprint("a的长度为:",#a)
print("---------------------")
local b = {}for i = 5,13 do b[i] = i endprint("b的长度为:",#b)
a的长度为:0---------------------b的长度为:13





数组长度为什么是0


细心的朋友们一定发现了上面的例子似乎和我们讲的有些不太一样,为什么a的长度会变成0了呢?这里我们就不得不再去了解一下lua 给table赋值的两种方式。



  • 初始化列表方式


  • 按索引赋值方式



显然我们的a表与b表没有使用初始化长度的方式来进行赋值。那么对于a,b两表,在赋值的过程中会不断的填充元素,导致表空间不足需要动态扩充。













































/*** inserts a new key into a hash table; first, check whether key's main ** position is free. If not, check whether colliding node is in its main ** position or not: if it is not, move colliding node to an empty place and ** put new key in its main position; otherwise (colliding node is in its main ** position), new key goes to an empty position. */static TValue *newkey (lua_State *L, Table *t, const TValue *key) { Node *mp = mainposition(t, key); if (!ttisnil(gval(mp)) || mp == dummynode) { Node *othern; Node *n = getfreepos(t); /* get a free place */ if (n == NULL) { /* cannot find a free place? */ rehash(L, t, key); /* grow table */ return luaH_set(L, t, key); /* re-insert key into grown table */ } lua_assert(n != dummynode); othern = mainposition(t, key2tval(mp)); if (othern != mp) { /* is colliding node out of its main position? */ /* yes; move colliding node into free position */ while (gnext(othern) != mp) othern = gnext(othern); /* find previous */ gnext(othern) = n; /* redo the chain with `n' in place of `mp' */ *n = *mp; /* copy colliding node into free pos. (mp->next also goes) */ gnext(mp) = NULL; /* now `mp' is free */ setnilvalue(gval(mp)); } else { /* colliding node is in its own main position */ /* new node will go into free position */ gnext(n) = gnext(mp); /* chain new position */ gnext(mp) = n; mp = n; } } gkey(mp)->value = key->value; gkey(mp)->tt = key->tt; luaC_barriert(L, t, key); lua_assert(ttisnil(gval(mp))); return gval(mp);}


在扩充的过程中会调用rehash 操作进行重新分配数组与哈希表的内存空间。












































static void rehash (lua_State *L, Table *t, const TValue *ek) { int nasize, na; int nums[MAXBITS+1]; /* nums[i] = number of keys between 2^(i-1) and 2^i */ int i; int totaluse; for (i=0; i<=MAXBITS; i++) nums[i] = 0; /* reset counts */ nasize = numusearray(t, nums); /* count keys in array part */ totaluse = nasize; /* all those keys are integer keys */ totaluse += numusehash(t, nums, &nasize); /* count keys in hash part */ /* count extra key */ nasize += countint(ek, nums); totaluse++; /* compute new size for array part */ na = computesizes(nums, &nasize); /* resize the table to new computed sizes */ resize(L, t, nasize, totaluse - na);}
static int computesizes (int nums[], int *narray) { int i; int twotoi; /* 2^i */ int a = 0; /* number of elements smaller than 2^i */ int na = 0; /* number of elements to go to array part */ int n = 0; /* optimal size for array part */ for (i = 0, twotoi = 1; twotoi/2 <*narray; i++, twotoi *= 2) { if (nums[i] > 0) { a += nums[i]; if (a > twotoi/2) { /* more than half elements present? */ n = twotoi; /* optimal size (till now) */ na = a; /* all elements smaller than n will go to array part */ } } if (a == *narray) break; /* all elements already counted */ } *narray = n; lua_assert(*narray/2 <= na && na <= *narray); return na;}

rehash 操作会调用computesizes 来统计数组部分不为nil的元素个数来计算最终要分配的空间长度。


对于表a来讲,在for循环插入元素时,不断进入computesizes 试图扩充数组长度,数组长度在1->2->4->8->16的过程中没有满足一个重要的条件,即if (a > twotoi/2) /* more than half elements present? */ 数组重新分配长度的原则在于数组利用效率高于50%。那么可以很容易的知道了为什么a表的sizearray 为0,而b表的sizearray 一下变成了13。


至此,文章开头的问题也有了完整的答案。


推荐阅读
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • Java程序设计第4周学习总结及注释应用的开发笔记
    本文由编程笔记#小编为大家整理,主要介绍了201521123087《Java程序设计》第4周学习总结相关的知识,包括注释的应用和使用类的注释与方法的注释进行注释的方法,并在Eclipse中查看。摘要内容大约为150字,提供了一定的参考价值。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文介绍了游标的使用方法,并以一个水果供应商数据库为例进行了说明。首先创建了一个名为fruits的表,包含了水果的id、供应商id、名称和价格等字段。然后使用游标查询了水果的名称和价格,并将结果输出。最后对游标进行了关闭操作。通过本文可以了解到游标在数据库操作中的应用。 ... [详细]
  • 本文介绍了如何使用python从列表中删除所有的零,并将结果以列表形式输出,同时提供了示例格式。 ... [详细]
  • 006_Redis的List数据类型
    1.List类型是一个链表结构的集合,主要功能有push,pop,获取元素等。List类型是一个双端链表的结构,我们可以通过相关操作进行集合的头部或者尾部添加删除元素,List的设 ... [详细]
author-avatar
婕小米
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有