• lua的table实现以及遍历方式


    最近遇到一件很郁闷的事情,游戏里面我老是当队长!后来发现是因为队伍里每个人的数据会以游戏的ID为key,其余要用到的数据为value,放在一个table里面。然后用pairs遍历这个table,一个个加入队伍,所以谁当队长实际上和pairs的遍历顺序有关,具体来说是和pairs找到的第一个元素有关。一时兴起,重新看了一下lua的table的实现,终于找到原因!
    一如既往,先报源码:
    Ltable.c     table的主要代码在这里
    Lobject.h     lua的基础对象的定义,包括table
    假设我的ID是100005(纯粹是个假设,我当然不可能把我真的ID报出来啦,不过我的ID和这个100005一样苦逼啊有木有)。假设队员的ID是100001,100002,100003,100004,经过试验用pairs来遍历这些key的顺序是100005,100002,100003,100004,100001,下面就看看为什么遍历顺序是这样的。
    先看看在Lobject.h中的关键数据结构:
    /*
    ** Common Header for all collectable objects (in macro form, to be
    ** included in other objects)
    */
    #define CommonHeader     GCObject *next; lu_byte tt; lu_byte marked
    ....
    typedef struct Table {
    CommonHeader;
    lu_byte flags;  /* 1<<p means tagmethod(p) is not present */
    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;
    lua的table实际上是一个数组和hash表的结合体,定义中我们只关心lsizenode,array,node,lastfree和sizearray,其余的部分是每个lua基本类型都有的部分。lsizenode,node,lastfree这三个属性存储的就是哈希表部分;array和sizearray存储的就是数组部分。数组部分比较简单,array就是数组本身,sizearray是数组的大小;哈希表部分的话,node指向哈希表起始地址,lsizenode是log2(node指向的哈希表节点数目),注意2^lsizenode不等于哈希表中存储变量的数目,因为哈希表是有可能有冲突的,所以一个哈希表节点是一个链表的表头,他可能对应多个存储变量。lastfree指向node里面最后一个未用的节点(这个用法很特别,后面会详细说)。
    /*
    ** Tagged Values
    */
    #define TValuefields     Value value; int tt
    ....
    typedef struct lua_TValue {
    TValuefields;
    } TValue;
    ...
    typedef union TKey {
    struct {
    TValuefields;
    struct Node *next;  /* for chaining */
    } nk;
    TValue tvk;
    } TKey;
    typedef struct Node {
    TValue i_val;
    TKey i_key;
    } Node;
    Node是哈希表中节点的结构,i_val是这个节点对应value,i_key是节点对应的key,其类型为TKey。TKey的定义很巧究(也很坑爹),这是一个union,TValue的定义实际上也是TValuefields这个宏,所以TKey的内存分布如下所示。
    |------------------------------|
    |value; |           |            |
    |----------   tvk   |            |
    |tt;       |           |      nk   |
    |-------------------|             |
    |next    |   next |             |
    |-------------------------------|
    画得很丑,将就着看吧。简单来说如果有TKey  a;那么
    a.tvk.value == a.nk.value
    a.tvk.tt == a.nk.tt
    lua之所以用这种方式来表示Node,感觉一个很重要的原因是为了适应不同参数类型的接口。
    TKey中tvk是这个key的值,nk中的next则指向key冲突的下一个节点。lua的hash表的hash算法比较特别,一般的hash表都是根据key算出hash(key),然后把这个key放在hash表的hash(key)位置上,如果有冲突的话,就放在hash(key)位置的链表上。
    但是lua的hash表中,如果有冲突的话,lua会找hash表中一个空的位置(从后往前找,假设为x),然后把新的key放在这个空的位置x上,并且让hash表中hash(key)处的节点的nk.next指向x。这个意思就是,假如有冲突的话,不用重新分配空间来存储冲突的key,而是利用hash表上未用过的空格来存储。但是这样会引入另外一个问题,本来key是不应该放在x的,假设有另外一个key2,hash(key2)算出来的位置也在x的话,那就表示本来x这个位置应该是给key2的,但是由于x被key占用了,导致key2没地方放了。这时候lua的处理方式是把key放到另外一个空格,然后让key2占回x。当hash表已经没有空格的时候,lua就会resize这个hash表。这样做的好处主要是不用动态申请内存空间,hash表初始化的时候有多少内存空间就用多少,不够就resize这个hash表。
    下面来看看table的具体实现,见Ltable.c:
    初始化table:
    Table *luaH_new (lua_State *L, int narray, int nhash) {
    Table *t = luaM_new(L, Table);
    luaC_link(L, obj2gco(t), LUA_TTABLE);
    t->metatable = NULL;
    t->flags = cast_byte(~0);
    /* temporary values (kept only if some malloc fails) */
    t->array = NULL;
    t->sizearray = 0;
    t->lsizenode = 0;
    t->node = cast(Node *, dummynode);
    setarrayvector(L, t, narray);
    setnodevector(L, t, nhash);
    return t;
    }
    一目了然,无需多说,array和hash部分默认都是0,然后用narray和nhash来初始化array和hash部分。array部分就初始化为narray长度的数组,hash部分就初始化为2^ceil(log2(nhash))长度的哈希表。table的哈希表的长度必须是2的幂,至于增长规则会在后面说到。
    table插入key:
    /*
    ** 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);
    }
    之前已经说过了,就是一个利用哈希表空洞来存储冲突节点的算法。其中有一些要注意的地方,首先是getfreepos这个函数:
    static Node *getfreepos (Table *t) {
    while (t->lastfree-- > t->node) {
    if (ttisnil(gkey(t->lastfree)))
    return t->lastfree;
    }
    return NULL;  /* could not find a free place */
    }
    这个函数会从后往前搜索hash表的空洞,找到的话就返回指向这个空洞的指针。lastfree在创建hash表的时候指向hash表最后一个元素。通过getfreepos可以知道hash表究竟还有没有空洞,如果没有的话,就要调用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);
    }
    rehash中,nums是用来统计key的数量分布的,他的定义也说的很清楚
    int nums[MAXBITS+1];  /* nums[i] = number of keys between 2^(i-1) and 2^i */
    numusearray和numusehash的目的是统计数组和hash表部分key的使用情况,把它更新到nums里面去。然后根据nums计算要申请多大的数组部分以及多大的hash表,算法在computesizes处:
    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;
    }
    nums就是key的分布情况,narray是所有key里面,可以放在array里面的key的个数(array大小最大只能到2^26,并且如果key不是整数不能放在array里)。computesizes的算法,简单来说就是找出一个最小的i,使其满足小于2^i的key的个数大于2^(i-1),这个2^i就是数组部分的大小。如果找不到满足条件的i,数组部分长度为0。这样做的话,就保证了数组部分包含尽量多的元素,同时使用率在一半以上。
    computesizes算完之后就调用resize把hash表和array都重新分配一次,lua的resize和redis相比简单很多,一次过就把元素都重新插入到新的hash表和数组里面去了。注意:由于插入元素会导致rehash,rehash会重新调整元素是放在数组还是放在hash表,所以一个元素无论原来是在数组还是hash表,rehash后都不能确定它是在数组还是hash表,这就说明了为啥对一个当作数组使用的table的指定key赋值后,ipairs遍历这个table的结果通常不靠谱。
    table的查找:
    /*
    ** main search function
    */
    const TValue *luaH_get (Table *t, const TValue *key) {
    switch (ttype(key)) {
    case LUA_TNIL: return luaO_nilobject;
    case LUA_TSTRING: return luaH_getstr(t, rawtsvalue(key));
    case LUA_TNUMBER: {
    int k;
    lua_Number n = nvalue(key);
    lua_number2int(k, n);
    if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */
    return luaH_getnum(t, k);  /* use specialized version */
    /* else go through */
    }
    default: {
    Node *n = mainposition(t, key);
    do {  /* check whether `key' is somewhere in the chain */
    if (luaO_rawequalObj(key2tval(n), key))
    return gval(n);  /* that's it */
    else n = gnext(n);
    } while (n);
    return luaO_nilobject;
    }
    }
    }
    如果key是string就调用luaH_getstr,如果是整数就调用luaH_getnum,默认的情况下找到key所在的节点,然后找到节点指向的链表中这个key的位置返回。luaH_getstr和luaH_getnum其实也是这个过程,只不过对string和number的哈希算法不同,number也有可能会放在数组部分而已。
    table的遍历:
    table的遍历分为ipairs和pairs,ipairs遍历数组部分,pairs遍历整个table。ipairs遍历顺序就是从0开始一次加1往后遍历table的数组部分。pairs的遍历实际上是调用luaH_next:
    int luaH_next (lua_State *L, Table *t, StkId key) {
    int i = findindex(L, t, key);  /* find original element */
    for (i++; i < t->sizearray; i++) {  /* try first array part */
    if (!ttisnil(&t->array[i])) {  /* a non-nil value? */
    setnvalue(key, cast_num(i+1));
    setobj2s(L, key+1, &t->array[i]);
    return 1;
    }
    }
    for (i -= t->sizearray; i < sizenode(t); i++) {  /* then hash part */
    if (!ttisnil(gval(gnode(t, i)))) {  /* a non-nil value? */
    setobj2s(L, key, key2tval(gnode(t, i)));
    setobj2s(L, key+1, gval(gnode(t, i)));
    return 1;
    }
    }
    return 0;  /* no more elements */
    }
    算法一目了然,先查数组部分,然后查哈希表,找到当前key的下一个key。所以遍历的结果是先遍历数组部分,然后遍历哈希表部分,哈希表部分实际上就是把node顺序遍历一次。
    分析到这里,已经基本知道原因了。number在lua里面是用double表示的,100001,100002,100003,100004,100005必定不能满足数组部分50%使用率的要求,所以都是放在哈希表里面。容纳这5个元素的哈希表的大小是8。lua的哈希number的哈希算法如下所示:
    /*
    ** hash for lua_Numbers
    */
    static Node *hashnum (const Table *t, lua_Number n) {
    unsigned int a[numints];
    int i;
    if (luai_numeq(n, 0))  /* avoid problems with -0 */
    return gnode(t, 0);
    memcpy(a, &n, sizeof(a));
    for (i = 1; i < numints; i++) a[0] += a[i];
    return hashmod(t, a[0]);
    }
    其中hashmod是计算a[0]%(哈希表大小-1)然后取t中这个位置的元素。所以100005之所以排在第一位是因为hashmod中计算出来100005的位置比其他元素的位置靠前,所以他放在靠前的位置,这样在pairs的时候,会首先遍历到100005。至此谜底完全解开。
    http://yulinlu.blog.163.com/blog/static/5881569820117351731462/

  • 相关阅读:
    9
    8
    7
    lua开发和调试环境
    MeshFilter mesh vs sharedMesh
    几种方法验证unity是否为development build
    Unity SetActive Event
    利用Data Vault对数据仓库进行建模(二)
    程序员如何圆飞行梦想(一)
    利用Data vault对数据仓库建模
  • 原文地址:https://www.cnblogs.com/byfei/p/14104511.html
Copyright © 2020-2023  润新知