#lua table 源码分析
lua使用table的单一结构,既可以做array,又可以成为hash,链表,树等结构,是一种简洁高效的使用形式。即使是对虚拟机来说,访问表项也是由底层自动统一操作的,因而用户不必考虑这种区别。表会根据其自身的内容自动动态地使用这两个部分:数组部分试图保存所有那些键介于1 和某个上限n之间的值。非整数键和超过数组范围n 的整数键对应的值将被存入散列表部分。
首先看下table的数据结构定义
(lobject.h)
319 /*
320 ** Tables
321 */
322
323 typedef union TKey {
324 struct {
325 TValuefields;
326 struct Node *next; /* for chaining */
327 } nk;
328 TValue tvk;
329 } TKey;
330
331
332 typedef struct Node {
333 TValue i_val;
334 TKey i_key;
335 } Node;
336
337
338 typedef struct Table {
339 CommonHeader;
340 lu_byte flags; /* 1<<p means tagmethod(p) is not present */
341 lu_byte lsizenode; /* log2 of size of `node' array */
342 struct Table *metatable;
343 TValue *array; /* array part */
344 Node *node;
345 Node *lastfree; /* any free position is before this position */
346 GCObject *gclist;
347 int sizearray; /* size of `array' array */
348 } Table;
array: lua表的数组部分起始位置的指针。
node:lua表hash数组的起始位置的指针。
sizearray:lua表数组部分的指针。
根据数据结构看:table的数组部分和hash部分是分别存储的。那么问题来了,什么样的数据会进去数组部分,什么样的数据会进入hash部分?
[0]是一定进入hash,[string]一定是hash的,[int]可能是array,也可能是hash。
是array还是hash的算法是:
数组在每一个2次方位置,其容纳的元素数量都超过了该范围的50%,能达到这个目标的话,那么Lua认为这个数组范围就发挥了最大的效率。
举个例子:
local p = {[0]=1,[1]=2,[2]=2,[4]=3}
print(#p)
local px = {[0]=1,[1]=2,[2]=2,[5]=3}
print(#px)
输出: 4
输出: 2
算法简说:
除去0,index 1, 2,4 一共3个元素。
算法 : 2^i < 3,i = 2, max_idx = 2^2 =4, index大于4的进入hash。
算法详解:
(ltable.c)
189 static int computesizes (int nums[], int *narray) {
190 int i;
191 int twotoi; /* 2^i */
192 int a = 0; /* number of elements smaller than 2^i */
193 int na = 0; /* number of elements to go to array part */
194 int n = 0; /* optimal size for array part */
195 for (i = 0, twotoi = 1; twotoi/2 < *narray; i++, twotoi *= 2) {
196 if (nums[i] > 0) {
197 a += nums[i];
198 if (a > twotoi/2) { /* more than half elements present? */
199 n = twotoi; /* optimal size (till now) */
200 na = a; /* all elements smaller than n will go to array part */
201 }
202 }
203 if (a == *narray) break; /* all elements already counted */
204 }
205 *narray = n;
206 lua_assert(*narray/2 <= na && na <= *narray);
207 return na;
208 }
nums[]: 构建的一个数组,根据index的大小,添加计数对应的位置,保证 2^(i-1) < index < 2^i。
narray:数组的size
另外: 对于数组的定义
local px = {[0]=1,[1]=2,[2]=2,[5]=3} --hash
local px = {1,2,2,3} --array
当对于数组关键节(i = 2^n)点进行定义,修改的时候才会触发computesizes,重新进行分配。
新的数组部分的大小是满足以下条件:
- 1到n 之间至少一半的空间会被利用(避免像稀疏数组一样浪费空间);
- 并且n/2+1到n 之间的空间至少有一个空间被利用(避免n/2 个空间就能容纳所有数据时申请n 个空间而造成浪费)。
当新的大小计算出来后,Lua 为数组部分重新申请空间,并将原来的数据存入新的空间。
这种混合型结构有两个优点。
- 存取整数键的值很快,因为无需计算散列值。
- 第二,也是更重要的,相比于将其数据存入散列表部分,数组部分大概只占用一半的空间,因为在数组部分,键是隐含的,而在散列表部分则不是。
结论就是,如果表被当作数组用,只要其整数键是紧凑的(非稀疏的),那么它就具有数组的性能,而且无需承担散列表部分的时间和空间开销,因为这种情况下散列表部分根本就不存在。相反,如果表被当作关联数组用,而不是当数组用,那么数组部分就可能不存在。这种内存空间的节省很重要,因为在Lua 程序中,常常创建许多小的表。