lua源码分析(一)揭开 table 的神秘面纱

       友情提醒:阅读本文前请先确保自己对哈希表有足够深入的理解,哈希表的详解可以参见以下这篇文章:Redis底层详解(一) 哈希表和字典

       lua 底层 table (也叫数组, 对象, 或者哈希表) 的实现分为两部分:数组部分 和 哈希部分。非负整数的键 候选放入数组部分。数组的实际大小为n。n的定义为 1到n中有超过 n/2 的元素被使用,且n的值最大。哈希部分的结点在物理地址上是连续的,通过next 偏移量将所有键值串联起来。

       redis 和 lua 的哈希表都是采用链地址来解决冲突的,只不过redis在冲突的时候需要每次申请一个新的结点(有内存申请的开销),lua 的 table 则是从静态的哈希表尾部往前找到第一个空闲结点来存放冲突结点,然后通过 next 偏移将两个结点连接起来,这点实现上,lua 更加高效。
       lua 的哈希表在实现上,遵循这样一个原则,当一个元素没有出现在它的哈希值计算出来的位置(即下文所指的 mainposition)时,则说明有一个冲突(哈希值相同)的元素占据了它的位置。那么它会自己选择一个位置,并且将所有 哈希值相同的元素链起来,所以当负载因子达到100%,性能也不会出现下降的情况。

      快速过一遍概念:
      1、哈希表是一个数组,所以也叫哈希数组;
      2、哈希表的数组长度一定是2的幂;
      3、哈希表的每个元素是一个联合结构体,这样就可以存布尔、整数、浮点数、指针、函数指针等任意类型的数据。每个元素有一个指针,用来冲突时指向哈希值相同的另一个元素。
      4、哈希值是利用哈希函数对给定的键计算出来的一个下标值,下标的范围为 [0, 哈希表长度)。
      5、哈希函数是一个函数,输入是键,输出是整数。
      6、哈希冲突是指两个不同的键计算得到相同的哈希值。
     

Table的数据结构定义

       首先来看下底层的 table 结构,定义在 lobject.h 中:

typedef struct Table {
    CommonHeader;
    lu_byte flags;            /* 1<<p means tagmethod(p) is not present */
    lu_byte lsizenode;        /* (1<<lsizenode)就是哈希表部分的长度 */
    unsigned int sizearray;   /* sizearray就是数组部分的长度 */
    TValue *array;            /* 'array'数组,即table的数组部分 */
    Node *node;               /* 'node'数组, 即table的哈希表部分 */
    Node *lastfree;           /* any free position is before this position */
    struct Table *metatable;
    GCObject *gclist;
}Table;

       CommonHeader 用于所有能被gc的对象,是个宏定义,可以暂时先忽略。lu_byte 就是 unsigned char。array 代表数组部分的首指针,node代表哈希部分的首指针,两者都是动态分配的。数组部分的长度记录在变量 sizearray 中;哈希部分的长度一定是2的幂(这是为了方便做 rehash 以及快速取模运算),所以哈希表的长度可以记录在一个字节中,即 lsizenode(实际哈希表长度为 2 的 lsizenode 次幂)。lastfree是一个指针,指向node数组中最后一个空闲的结点,会在下文的 getfreepos 函数中进行讲解,用于哈希表分配新的结点时用。flag、metatable、gclist 预留。数组部分的类型为TValue,是 lua 的基础类型。定义在 lobject.h 中:

/*
** lua 基础类型的联合体
*/
typedef union Value {
    GCObject *gc;    /* collectable objects */
    void *p;         /* light userdata */
    int b;           /* booleans */
    lua_CFunction f; /* light C functions */
    lua_Integer i;   /* integer numbers */
    lua_Number n;    /* float numbers */
}Value;

typedef struct lua_TValue {
    Value value_; 
    int tt_;
}TValue;

       成员变量 value_ 是个 union 联合体,它可以是整数、浮点数、函数、指针等任意类型。tt_ 存储了这个 union 结构体实际的类型,类型判断是用的一些宏定义和位运算来完成的,主要是 checktag 和 checktype 两个宏定义衍生出一系列的类型判断,详细源码可以在 lobject.h 中找到,这里不再累述。
       再来看哈希部分的结构为Node,同样定义在 lobject.h 中:

typedef union TKey {
    struct {
        Value value_; 
        int tt_;
        int next;  /* 下一个node的偏移量 */
    }nk;
    TValue tvk;
}TKey;

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

       Node 由键和值两部分组成,键 TKey 是一个联合体(为什么这里要引入这么一个联合体呢?定义两遍?玄机在这个 next 上面,请翻看源码 setnodekey 的实现,上面有一条注释会给你答案);值就是基础的 lua 类型。其中 next 是连接两个键的关键,如下图所示:

       除了可以用下标访问到每个Node结点,还可以通过 next 偏移量,对指针进行偏移操作获得当前结点链接的下一个结点。所以哈希部分的物理地址虽然连续,实际还是一个链表。

Table的创建

       Table 创建的 API 为 luaH_new,实现如下:

Table *luaH_new (lua_State *L) {
    GCObject *o = luaC_newobj(L, LUA_TTABLE, sizeof(Table));
    Table *t = gco2t(o);
    t->metatable = NULL;
    t->flags = cast_byte(~0);
    t->array = NULL;
    t->sizearray = 0;
    setnodevector(L, t, 0);
    return t;
}

       t 为创建的 table 对象,初始化数组部分为空,哈希部分调用 setnodevector 进行初始化,实现如下:

static void setnodevector (lua_State *L, Table *t, unsigned int size) {
    if (size == 0) {                               /* 初始化的时候 */
        t->node = cast(Node *, dummynode);         /* 空结点'dummynode' */
        t->lsizenode = 0;                          /* 哈希表长度为1 */
        t->lastfree = NULL;                 
    }
    else {
        int i;
        int lsize = luaO_ceillog2(size);           /* 等同于ceil(log2(size)) */
        if (lsize > MAXHBITS)
            luaG_runerror(L, "table overflow");
        size = twoto(lsize);                       /* size = (1<<lsize) */
        t->node = luaM_newvector(L, size, Node);   /* 分配内存 */
        for (i = 0; i < (int)size; i++) {
            Node *n = gnode(t, i);
            gnext(n) = 0;
            setnilvalue(wgkey(n));
            setnilvalue(gval(n));
        }
        t->lsizenode = cast_byte(lsize);
        t->lastfree = gnode(t, size);
    }
}

       当 size 为0的情况比较简单,整个哈希表只有一个 dummynode;那么来讨论 size 不为0的情况,首先计算需要创建的哈希表的实际长度,利用的是 luaO_ceillog2 函数计算得到 lsize,这个函数实现效果等同于 ceil(log2(size)),即取以2为底size的对数的上整。得到的值 lsize 就是哈希表尺寸的指数部分,实际哈希表尺寸就是 (1<<lsize)。利用 luaM_newvector 分配内存,然后就是一些初始化工作了。gnode、gnext、wgkey、gval 都是定义在 ltable.h 的宏定义:

/* 获取 table t 的哈希部分的第i个node结点的地址 */
#define gnode(t,i)	(&(t)->node[i])

/* 获取 Node n 的值部分的地址 */
#define gval(n)		(&(n)->i_val)

/* 获取 Node n 指向的下一个Node的下标偏移 */
#define gnext(n)	((n)->i_key.nk.next)

/* 获取 Node n 的键,且不可写 */
#define gkey(n)		cast(const TValue*, (&(n)->i_key.tvk))

/* 可写版本的 'gkey',但不适用于所有类型 */
#define wgkey(n)	(&(n)->i_key.nk)

       初始化其实就是将哈希表中所有的 Node 的 key 和 value 的类型都置为 nil (调用 setnilvalue),并且将 next 字段设置为0(即哈希表链表的每个元素的 next 指针都指向自己)。(将 lastfree 指向了数组外的地址???读者可以在 getfreepos 函数中寻找答案

       那么 table 中各种各样的 key 类型是如何映射到 node 数组中的呢?这里引入一个 main position 的概念,它的含义就是根据 key 计算出的哈希值并对哈希表长度进行取模后映射到的 node 数组中对应的 Node 的地址。

Main position

       mainposition 的实现在 ltable.c 中定义,实现如下:

static Node *mainposition (const Table *t, const TValue *key) {
    switch (ttype(key)) {
        case LUA_TNUMINT:
            return hashint(t, ivalue(key));
        case LUA_TNUMFLT:
            return hashmod(t, l_hashfloat(fltvalue(key)));
        case LUA_TSHRSTR:
            return hashstr(t, tsvalue(key));
        case LUA_TLNGSTR:
            return hashpow2(t, luaS_hashlongstr(tsvalue(key)));
        case LUA_TBOOLEAN:
            return hashboolean(t, bvalue(key));
        case LUA_TLIGHTUSERDATA:
            return hashpointer(t, pvalue(key));
        case LUA_TLCF:
            return hashpointer(t, fvalue(key));
        default:
            lua_assert(!ttisdeadkey(key));
            return hashpointer(t, gcvalue(key));
    }
}

       其实就是根据 key 的类型,计算哈希函数,将得到的哈希值对哈希表长度进行取模,然后利用 gnode 获取对应位置的 Node 的地址。hashint、hashstr、hashboolean等等的hash函数都是一些宏定义,我们来看下具体的定义:

#define hashint(t,i)		hashpow2(t, i)
#define hashstr(t,str)		hashpow2(t, (str)->hash)
#define hashboolean(t,p)	hashpow2(t, p)

       这三个宏定义的参数类似,第一个参数 t 是 table 指针,第二个参数是根据实际类型获取到的实际对象,它可以是整型、字符串或者布尔类型(布尔的底层存储结构也是int,参见上文 Value 的定义)。然后统一调用另一个宏定义 hashpow2,定义如下:

#define hashpow2(t,n)		(gnode(t, lmod((n), sizenode(t))))

       lmod 和 sizenode 的宏定义可以在 lobject.h 文件中找到:

#define lmod(s,size) (check_exp((size&(size-1))==0, (cast(int, (s) & ((size)-1)))))

#define twoto(x)	(1<<(x))
#define sizenode(t)	(twoto((t)->lsizenode))

       实现都比较简单,check_exp 的作用是确保第一个参数的条件成立的情况下执行第二个参数,size&(size-1)==0 就是确保 size 是 2 的幂,这样&运算就可以等同于取模了。那么 hashpow2 的作用就是之前提到的,通过传入的整数,计算得到一个Node的指针。整数取模是想当然的,那么字符串呢?lua 的字符串会有一个成员对象 hash,这个就是字符串的哈希值了,哈希值计算如下,实现在 lstring.c 中:

unsigned int luaS_hash (const char *str, size_t l, unsigned int seed) {
    unsigned int h = seed ^ cast(unsigned int, l);
    size_t step = (l >> LUAI_HASHLIMIT) + 1;
    for (; l >= step; l -= step)
        h ^= ((h<<5) + (h>>2) + cast_byte(str[l - 1]));
    return h;
}

       哈希值的计算因人而异,实现变幻莫测,层出不穷,就不再详述了。至于为什么要这么实现,不是本文讨论的重点,你只需要知道一个字符串通过某些计算可以变成一个整型(当然,无可避免的,两个不同的字符串可以计算得到相同的值),需要了解更多关于哈希值计算的,可以自行百度。

       继续回到 mainposition 这个函数,对于浮点数、指针类型、函数类型的对象,采用了另一套实现。对于函数,也可以理解成函数指针,所以函数和指针取mainposition的方式都是调用了 hashpointer 这个宏定义:

/* 指针向无符号整型的转换:哈希专用 */
#define point2uint(p)	((unsigned int)((size_t)(p) & UINT_MAX))


/* 对于某些类型,最好不要对2的幂取模,因为它本身有很多2的因子 */
#define hashmod(t,n)	(gnode(t, ((n) % ((sizenode(t)-1)|1))))


#define hashpointer(t,p)	hashmod(t, point2uint(p))

       我们看到,指针类型的取模是取的指针值转换成无符号整型后对 (哈希表长度-1)进行取模,那为什么还要“位或”上1呢?因为当哈希表长度为1时,如果不“位或”上1,取模可能导致除0错。hashmod 主要是为了避免这样一种情况,当被取模的数永远不可能为奇数时,如果对2的幂取模,那么得到值也永远不可能为奇数,导致哈希表的一半空间被浪费掉。

       最后我们来看浮点数的哈希,核心也是采用 hashmod 进行取模找位置,只不过传入的参数并不是这个浮点数本身(因为它不能像整型一样进行取模运算), 所以我们通过 l_hashfloat 函数进行了一层转换,具体实现如下:

static int l_hashfloat (lua_Number n) {
    int i;
    lua_Integer ni;
    n = l_mathop(frexp)(n, &i) * -cast_num(INT_MIN);
    if (!lua_numbertointeger(n, &ni)) {  /* is 'n' inf/-inf/NaN? */
        lua_assert(luai_numisnan(n) || l_mathop(fabs)(n) == cast_num(HUGE_VAL));
        return 0;
    }
    else {  /* normal case */
        unsigned int u = cast(unsigned int, i) + cast(unsigned int, ni);
        return cast_int(u <= cast(unsigned int, INT_MAX) ? u : ~u);
    }
}

        主要看 frexp 这个函数,它把一个浮点数分解为尾数和指数(浮点数的表示是 尾数*(2^指数) ),指数是指针传参,尾数是返回值,即代码中 n为尾数(还是浮点数),i为指数(为整数)。最后的返回值就是 n*INT_MAX + i。当得到的值超过 INT_MAX 的时候,进行取反操作,使得得到值永远都落在 [0, INT_MAX] 范围内(避免之后取模的时候出现负数导致映射到数组下标的时候导致数组下标越界)。

Table 键的生成

        接下来,我们看下 table 的键的实现机制,生成一个新的键的逻辑是由 luaH_newkey 完成的,实现在 ltable.c 中,由于代码较长,我先分成几个部分来阐述,最后再给出代码。这里 t 为 Table 类型的指针,key 是常量的TValue指针。

        1、如果 key 是nil,则抛出异常 "table index is nil";否则转2;
        2、如果 key 是浮点数,则进行以下判断:
             a) key 能够无损转成 lua 整数类型,则直接将 key 转换成 lua 整数;
             b) key 如果是 NaN(Not a Number,比如无穷大除以无穷大),抛出异常 "table index is NaN";
        3、利用 mainposition(t, key) 根据 key 的哈希值在 t 中找到一个位置 mp,类型为 Node 的指针;
        4、如果 t 的哈希表是一个空表 或者 mp 已经被之前的占据 (产生冲突),则进行以下判断:
             a) 获取空闲结点 f (调用 getfreepos 函数);
             b) 如果没有找到,说明本次哈希表已满,进行哈希表翻倍( 即 rehash 的操作),然后调用 luaH_set (t, key) 将 key 插入到新的哈希表中(luaH_set的实现见下文暂且不展开来说);否则转c);
             c) 能够走到这一步,说明什么?我们来回顾一下,以免众多文字,逻辑错乱。没错,能够走到这一步,说明 mp 这个位置已经有其他元素占据了,那么通过 gkey(mp) 就可以得到这个位置是被哪个 key 占据的,然后我们再利用 mainposition(t, gkey(mp) ) 得到之前占据这个位置的元素的 mainposition,存到 othern 中,然后判断 othern 和 mp是否相等,如果相等转 d),不相等转 e);
             d) othern  == mp,空闲结点 f 预留给新的 key,并且插入到到原有哈希链中,如图所示:

             e) othern  != mp,找到 mp 的前驱结点 frep,将 frep 指向空闲结点 f ,f 指向 mp 的后继结点,并且将 mp 上所有的数据都拷到空闲结点 f 上。然后将 mp 设为 nil 空结点,如图所示(通俗的讲,就是 mp 这个位置需要留给新的 key,所以我们把它原有的数据拷到空闲结点 f 上,并且不改变原有的链接关系):

        5、经过上面的一顿操作猛如虎以后,mp 的位置上已经空出来了,直接调用 setnodekey 将传入的 key 设置到 mp 的 i_key 字段即可(注意:setnodekey 函数不改变 i_key 的 next 字段)。
        好了,啰嗦了一大堆,我们来看下源码是如何实现的,代码在 ltable.c 中(注释中的编号对应上文的步骤编号):

TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) {
    Node *mp;
    TValue aux;
    if (ttisnil(key))                                          
        luaG_runerror(L, "table index is nil");                         /* 1 */
    else if (ttisfloat(key)) {
        lua_Integer k;
        if (luaV_tointeger(key, &k, 0)) {                               /* 2.a */
            setivalue(&aux, k);
            key = &aux;  
        }
        else if (luai_numisnan(fltvalue(key)))                          
            luaG_runerror(L, "table index is NaN");                     /* 2.b */
    }

    mp = mainposition(t, key);                                          /* 3 */
    if (!ttisnil(gval(mp)) || isdummy(t)) {
        Node *othern;
        Node *f = getfreepos(t);                                        /* 4.a */
        if (f == NULL) { 
            rehash(L, t, key);      
            return luaH_set(L, t, key);                                 /* 4.b */
        }
        lua_assert(!isdummy(t));
        othern = mainposition(t, gkey(mp));                             /* 4.c */

        if (othern != mp) {                                             /* 4.e */
            while (othern + gnext(othern) != mp)      /* 寻找mp的前驱结点 */
                othern += gnext(othern);
            gnext(othern) = cast_int(f - othern);     /* othern 指向 f */
            *f = *mp;                                 /* 将冲突的结点的内容挪到空闲结点f */
            if (gnext(mp) != 0) {
                gnext(f) += cast_int(mp - f);         /* f 指向 mp */
                gnext(mp) = 0;                        
            }
            setnilvalue(gval(mp));                    /* 目前为止,mp已经成为空闲结点了 */
        }else {                                                         /* 4.d */
            if (gnext(mp) != 0)
                gnext(f) = cast_int((mp + gnext(mp)) - f);  /* f 指向 mp的后继 */
            else 
                lua_assert(gnext(f) == 0); 
            gnext(mp) = cast_int(f - mp);                   /* mp 指向 f */
            mp = f;                                         /* mp 就是空闲结点 */
        }
    }
    setnodekey(L, &mp->i_key, key);                                       /* 5 */
    luaC_barrierback(L, t, key);
    lua_assert(ttisnil(gval(mp)));
    return gval(mp);
}

        上面这个函数应该是 table 中最复杂也是最难理解的函数了,能看到这里完全理解,也算得上是英雄豪杰了(此处应有掌声)。那么我们接下来梳理一下,在生成 key 的时候有两个函数 luaH_set 和 rehash,并没有做详细的介绍,现在就来看看这两个函数的实现。luaH_set 实现很短,如下:

TValue *luaH_set (lua_State *L, Table *t, const TValue *key) {
    const TValue *p = luaH_get(t, key);
    if (p != luaO_nilobject)
        return cast(TValue *, p);
    else
        return luaH_newkey(L, t, key);
}

        luaH_set 核心是调用 luaH_get 从 t 中获取值, 不存在则调用 luaH_newkey 插入一个新的 key,否则转换成非常量类型后返回。那么还是要来看 luaH_get 究竟是如何实现的。

Table 的查找

        luaH_get (Table *t, const TValue *key) 是从 t 中查找键为 key 的值,实现在 ltable.c 中:

const TValue *luaH_get (Table *t, const TValue *key) {
    switch (ttype(key)) {
        case LUA_TSHRSTR: return luaH_getshortstr(t, tsvalue(key));
        case LUA_TNUMINT: return luaH_getint(t, ivalue(key));
        case LUA_TNIL: return luaO_nilobject;
        case LUA_TNUMFLT: {
            lua_Integer k;
            if (luaV_tointeger(key, &k, 0)) /* index is int? */
                return luaH_getint(t, k);  /* use specialized version */
            /* else... */
        }       /* FALLTHROUGH */
        default:
            return getgeneric(t, key);
    }
}

        这个接口对不同的 lua 类型的key进行分类讨论,短字符串调用 luaH_getshortstr 接口获取值,整数类型 或者 如果能够无损转成整数的浮点数类型调用 luaH_getint,剩下的就交给 getgeneric 去获取了。
        1、luaH_getshortstr 实现如下:

const TValue *luaH_getshortstr (Table *t, TString *key) {
    Node *n = hashstr(t, key);
    lua_assert(key->tt == LUA_TSHRSTR);
    for (;;) {
        const TValue *k = gkey(n);
        if (ttisshrstring(k) && eqshrstr(tsvalue(k), key))
            return gval(n);
        else {
            int nx = gnext(n);
            if (nx == 0)
                return luaO_nilobject;
            n += nx;
        }
    }
}

        短字符串的长度定义是 LUAI_MAXSHORTLEN,它的值是40。当长度不超过40的字符串会存在lua的底层stringtable中,所以比较两个字符串相等只要比较他们的指针相等即可(参见 eqshrstr 这个宏定义),定成40是时间和空间上的权衡。
       luaH_getshortstr 函数其实就是获取这个字符串的哈希值,然后得到它的mainposition,通过mainposition在它所在的哈希链上找键和它一致的那个node,返回它所在的值(即 gval(n))。哈希链的迭代是通过 next 偏移量加上本身的地址来实现的。

        2、luaH_getint 实现如下:

const TValue *luaH_getint (Table *t, lua_Integer key) {
    /* (1 <= key && key <= t->sizearray) */
    if (l_castS2U(key) - 1 < t->sizearray)
        return &t->array[key - 1];
    else {
        Node *n = hashint(t, key);
        for (;;) {
            if (ttisinteger(gkey(n)) && ivalue(gkey(n)) == key)
                return gval(n);  /* that's it */
            else {
                int nx = gnext(n);
                if (nx == 0) break;
                n += nx;
            }
        }
        return luaO_nilobject;
    }
}

        当键为整数时,首先判断是否在数组部分的范围内,如果在则直接下标索引;否则和短字符串的处理一致(遍历哈希链)。       

       3、getgeneric 是更加通用的获取值的接口,实现如下:

static const TValue *getgeneric (Table *t, const TValue *key) {
    Node *n = mainposition(t, key);
    for (;;) {
        if (luaV_rawequalobj(gkey(n), key))
            return gval(n);
        else {
            int nx = gnext(n);
            if (nx == 0)
                return luaO_nilobject;
            n += nx;
        }
    }
}

         这个接口用于非整数的键用于获取值的情况,luaV_rawequalobj 是通用的比较两种 TValue 类型是否相等的接口。
         总而言之,Table 的键值查找,就是通过 key 算出它的 mainposition,然后在所有 mainposition 相等的 node 串起来的这条哈希链上进行遍历,找到一个键相等的进行返回。

空闲结点获取

         接下来讲讲 table 是如何得到一个空闲的结点的,简而言之,就是当哈希表产生冲突的时候,需要生成一个新的结点来放数据,如果每次冲突,重新分配一个 Node 结点的内存,会带来性能开销。所以 lua 的 table 采用了一种很巧妙的方式。实现在 getfreepos 中:

static Node *getfreepos (Table *t) {
    if (!isdummy(t)) {
        while (t->lastfree > t->node) {
            t->lastfree--;
            if (ttisnil(gkey(t->lastfree)))
                return t->lastfree;
        }
    }
    return NULL;
}

        用一个 lastfree 指针来指向 node 数组中的空闲结点,lastfree 初始化在 node 数组的结尾,每次调用时从数组从后往前遍历找到第一个空闲结点返回。如果找不到,说明 node 数组已经被占满,需要 rehash 了。那么,接下来就来说说 rehash 。

rehash

        整个 rehash 的过程用到5个函数:numusearray、numusehash、computesizes、luaH_resize。首先介绍这4个函数,有助于后续对 rehash 的理解。
        1、numusearray 的函数实现如下:

static unsigned int numusearray (const Table *t, unsigned int *nums) {
    int lg;
    unsigned int ttlg;       /* 2^lg */
    unsigned int ause = 0;   /* 所有 'nums' 的和 */
    unsigned int i = 1;      /* 遍历所有数组部分的下标 */
    for (lg = 0, ttlg = 1; lg <= MAXABITS; lg++, ttlg *= 2) {
        unsigned int lc = 0;  /* counter */
        unsigned int lim = ttlg;
        if (lim > t->sizearray) {
            lim = t->sizearray;
            if (i > lim)
                break;
        }
        /* 统计 (2^(lg - 1), 2^lg] 中的元素个数 */
        for (; i <= lim; i++) {
            if (!ttisnil(&t->array[i-1]))
                lc++;
        }
        nums[lg] += lc;
        ause += lc;
    }
    return ause;
}

        函数返回值 ause 代表 t 的数组部分中所有非 nil 键的数量。同时返回传参 'nums',nums[i] 代表在 (2^(i - 1), 2^i] 范围内的所有非nil键的数量。那么,所有 nums[i] 的和就是 ause。

        2、numusehash 的函数实现如下:

static int numusehash (const Table *t, unsigned int *nums, unsigned int *pna) {
    int totaluse = 0;  /* total number of elements */
    int ause = 0;  /* elements added to 'nums' (can go to array part) */
    int i = sizenode(t);
    while (i--) {
        Node *n = &t->node[i];
        if (!ttisnil(gval(n))) {
            ause += countint(gkey(n), nums);
            totaluse++;
        }
    }
    *pna += ause;
    return totaluse;
}

猜你喜欢

转载自blog.csdn.net/WhereIsHeroFrom/article/details/83538029
今日推荐