Erlang进程字典底层实现剖析

进程字典的结构:

typedef struct proc_dict {
    unsigned int sizeMask;   //  掩码,用于计算hash值落到data的索引值
    unsigned int usedSlots;  //  可以使用的插槽数(不等于已经被使用的插槽数)
    unsigned int arraySize;  //  data数组的长度
    unsigned int splitPosition;
    Uint numElements;        //  已经被使用的插槽数
    Eterm data[1];
} ProcDict;

put操作的源码:

static Eterm pd_hash_put(Process *p, Eterm id, Eterm value)
{
//  id为put(Key,Value)的Key值  value为Value值
    unsigned int hval;
    Eterm *hp;
    Eterm *tp;
    Eterm *bucket;
    Eterm tpl;
    Eterm old;
    Eterm old_val = am_undefined;
    Eterm tmp;
    int needed;
    int new_key = 1;

    if (p->dictionary == NULL) {
    //  当前进程字典为空时,初始化结构
    //  INITIAL_SIZE 值为8
        ensure_array_size(&p->dictionary, INITIAL_SIZE);
        p->dictionary->usedSlots = INITIAL_SIZE;
        p->dictionary->sizeMask = INITIAL_SIZE*2 - 1;
        p->dictionary->splitPosition = 0;
        p->dictionary->numElements = 0;
    }   
    //  hval为id转换后,在data中的索引值, 即在data数组中的第几项
    hval = pd_hash_value(p->dictionary, id);
    //  ARRAY_GET_PTR宏:p->dictionary->data[hval]
    bucket = ARRAY_GET_PTR(p->dictionary, hval);
    //  获取Key需要插入的插槽的当前值
    old = *bucket;
    //{Key,Value}构成的tuple需要3个word
    needed = 3;         
    if (is_boxed(old)) {
        // 若旧值为box类型,加入{Key,Value}后,要改为list,需要额外4个word(list每一项占2个word)
        needed += 2+2;  
    } else if (is_list(old)) {
        i = 0;
        // 遍历旧list,看Key是否有旧值
        // TCAR:取列表当前值; TCDR:取列表下一项地址
        for(tmp = old; tmp != NIL && !EQ(tuple_val(TCAR(tmp))[1], id); tmp = TCDR(tmp)){ ++i;}
        if (is_nil(tmp)){
            // 没有旧值,则将{Key,Value}插在列表头,需要2个word
            i = -1;
            needed += 2;
        } else {
            // 有旧值,则旧值前的数据需要复制,所以需要2*(i+1)个word
            needed += 2*(i+1);
        }
    }
    // 判断剩余空间是否足够,不够则触发垃圾回收
    if (HeapWordsLeft(p) < needed) {
        Eterm root[3];
        root[0] = id;
        root[1] = value;
        root[2] = old;
        erts_garbage_collect(p, needed, root, 3);
        id = root[0];
        value = root[1];
        old = root[2];
    }
    //  创建{Key,Value}的tuple
    hp = HeapOnlyAlloc(p, 3);
    tpl = TUPLE2(hp, id, value);

    //  核心  更新dictionary逻辑
    if (is_nil(old)) {
        // 若无旧数据,直接插入,numElements+1
        ARRAY_PUT(p->dictionary, hval, tpl);
        ++(p->dictionary->numElements);
    } else if (is_boxed(old)) {
        if (EQ(tuple_val(old)[1],id)) {
            // 若有旧数据,且旧数据的Key和当前Key相等,则替换
            ARRAY_PUT(p->dictionary, hval, tpl);
            return tuple_val(old)[2];
        } else {
            // 若有旧数据,且旧数据Key不等,则构造列表
            hp = HeapOnlyAlloc(p, 4);
            tmp = CONS(hp, old, NIL);
            hp += 2;
            ++(p->dictionary->numElements);
            ARRAY_PUT(p->dictionary, hval, CONS(hp, tpl, tmp));
            hp += 2;
        }
    } else if (is_list(old)) {
        if (i == -1) {
            // 若旧数据已经是列表,且不存在相同的Key,则将当前{Key,Value}置于列表头
            hp = HeapOnlyAlloc(p, 2);
            ARRAY_PUT(p->dictionary, hval, CONS(hp, tpl, old));
            hp += 2;
            ++(p->dictionary->numElements);
        } else {
            // i的值为需要被替换的值,在列表中的序号
            Eterm nlist;
            int j;
            // 申请内存
            hp = HeapOnlyAlloc(p, (i+1)*2);
            // 列表遍历到位置i,i后的列表数据是不需要被重建的
            for (j = 0, nlist = old; j < i; j++, nlist = TCDR(nlist)) {
            ;
            }
            // nlist是不需要被重建的部分
            nlist = TCDR(nlist);
            // 重建list
            for (tmp = old; i-- > 0; tmp = TCDR(tmp)) {
                nlist = CONS(hp, TCAR(tmp), nlist);
                hp += 2;
            }
            // 将插入的值放到列表头
            nlist = CONS(hp, tpl, nlist);
            hp += 2;
            ARRAY_PUT(p->dictionary, hval, nlist);
            // 这段逻辑其实可以优化,直接替换不用重建列表应该也是可以的
            return tuple_val(TCAR(tmp))[2];
        }
    } else {
        erl_exit(1, "Damaged process dictionary found during put/2.");
    }
    if (p->dictionary->usedSlots <= p->dictionary->numElements) {
        //  增长字典的插槽数
        grow(p);
    }
    return am_undefined;
}

进程字典空间增长的实现源码:

static void grow(Process *p)
{
    unsigned int i,j;
    unsigned int steps = (p->dictionary->usedSlots / 4) & 0xf;
    Eterm l1,l2;
    Eterm l;
    Eterm *hp;
    unsigned int pos;
    unsigned int homeSize;
    int needed = 0;
    ProcDict *pd = p->dictionary;

    if (steps == 0)
        steps = 1;
    // 增长后空间超过最大值,则return
    // MAX_HASH 值为 1342177280
    if ((MAX_HASH - steps) <= pd->usedSlots) {
        return;
    }
    // 扩充dictionary插槽,扩充规则:使用二分法在 tab 数组中找到第一个大于等于pd->usedSlots + steps的值为新的dictionary->data数组长度
    // steps为增加的使用插槽数, 使用的插槽数(usedSlots)和dictionary->data数组长度(arraySize)是两个概念, usedSlots =< arraySize
    ensure_array_size(&p->dictionary, pd->usedSlots + steps);
    pd = p->dictionary;
    // usedSlots变长,一些数据放置的插槽可能需要改变
    // splitPosition为上次分裂位置记录
    pos = pd->splitPosition;
    homeSize = pd->usedSlots - pd->splitPosition;
    for (i = 0; i < steps; ++i) {
        if (pos == homeSize) {
            homeSize *= 2;
            pos = 0;
        }
        l = ARRAY_GET(pd, pos);
        pos++;
        // 若l值为列表,则需要 2*列表长度的空间 (l中某些值可能会映射到别的插槽,所以l需要分裂)
        // 若l值不是列表,需要挪位置的话,修改位置就好,不需要申请新的空间
        if (is_not_tuple(l)) {
            while (l != NIL) {
                needed += 2;
                l = TCDR(l);
            }
        }
    }
    // 堆区剩余空间不够则进行垃圾回收
    if (HeapWordsLeft(p) < needed) {
        BUMP_REDS(p, erts_garbage_collect(p, needed, 0, 0));
    }
    homeSize = pd->usedSlots - pd->splitPosition;
    for (i = 0; i < steps; ++i) {
        //  此处算法是为了计算哪些位置的数据需要调整插槽
        /*  我们先看计算插槽的算法: temp = hx & sizeMask; if(temp >= usedSlots) then hx & (sizeMask >> 1) else temp;
            在sizeMask没有改变的情况下:
                {分析:    
                    旧规则:temp = hx & sizeMask; if(temp >= usedSlots) then hx & (sizeMask >> 1) else temp;
                    新规则:temp = hx & sizeMask; if(temp >= usedSlots + 1) then hx & (sizeMask >> 1) else temp;
                    只有 temp==usedSlots 时,按新旧规则会映射到不同插槽,所有只有该条件下的插槽需要调整位置
                }
                当usedSlots增加时, 所有插槽中只有满足 temp == usedSlots 的需要调整位置,
                即usedSlots增加前, 插槽为hx & (sizeMask >> 1); usedSlots增加后,插槽需要调整到hx & sizeMask,
                也就是要将 hx & (sizeMask >> 1) 移动到 hx & sizeMask(== usedSlots),
                因为 sizeMask 始终保持 pow(2,n)-1 的形式,
                所以每次usedSlots增长时,需要判断插槽 usedSlots-((sizeMask>>1)+1) 的值是否需要移动至插槽 usedSlots;
                (举个例子, 当usedSlots由9增加到10时,需要判断插槽1的数据是否需要移动至插槽9。注:插槽从0开始)
            当usedSlots==sizeMask+1后,sizeMask需要进行调整(左移一位后低位补1):
            (注:为什么usedSlots==sizeMask+1后才调整sizeMask, 因为插槽是从0开始的,如usedSlots等于16,sizeMask等于15时,哈希值会映射到0到15中,sizeMask是够用的)
                {分析:  
                    条件A:usedSlots==sizeMask+1,
                    旧规则:oldtemp = hx & sizeMask; if(oldtemp >= usedSlots) then hx & (sizeMask >> 1) else oldtemp;
                        在条件A下转换为:oldtemp = hx & sizeMask; return oldtemp;
                    新规则:newtemp = hx & (sizeMask<<1); if(newtemp >= usedSlots + 1) then hx & sizeMask else newtemp;
                        在条件A下转换为:newtemp = hx & (sizeMask+1) + oldtemp; if(newtemp >= usedSlots + 1) then oldtemp else newtemp;
                    当newtemp<usedSlots + 1 时, 旧规则放置于插槽oldtemp位置的数据需要转移到newtemp,
                        hx & (sizeMask+1) + oldtemp < usedSlots + 1 ==> hx & usedSlots + oldtemp < usedSlots + 1
                    推得只有 oldtemp < 1,即oldtemp等于0时,需要调整插槽
                    oldtemp==0时, newtemp取值为 hx & (sizeMask+1) = usedSlots
                }
                sizeMask调整时,需要判断 插槽0的数据是否转移到插槽usedSlots
                (举个例子,当usedSlots由16增加到17时,需要将插槽0的数据,移动至插槽16。)
            因此可以整理出算法:
                按上述分析整理出的算法可能会和源码实现有所出入,但整体逻辑是一致的
                splitPosition其实是可以usedSlots和sizeMask计算出来的[注:usedSlots-((sizeMask>>1)+1)],源码直接使用一个字段记录下来,可以省掉一些计算消耗
        */
        if (pd->splitPosition == homeSize) {
            homeSize *= 2;
            pd->sizeMask = homeSize*2 - 1;
            pd->splitPosition = 0;
        }
        pos = pd->splitPosition;
        ++pd->splitPosition;
        ++pd->usedSlots;
        l = ARRAY_GET(pd, pos);
        if (is_tuple(l)) {
            // 旧值为tuple,计算新的位置不等于当前位置
            if (pd_hash_value(pd, tuple_val(l)[1]) != pos) {
                // 将l插入到新位置
                ARRAY_PUT(pd, pos + homeSize, l);
                // l所属旧位置置空
                ARRAY_PUT(pd, pos, NIL);
            }
        } else {
            l2 = NIL;
            l1 = l;
            // 计算列表长度,申请空间
            for (j = 0; l1 != NIL; l1 = TCDR(l1))
                j += 2;
            hp = HeapOnlyAlloc(p, j);
            while (l != NIL) {
                // pos不变的数据添加到l1列表, 需要移动到新位置的数据添加到l2列表
                if (pd_hash_value(pd, tuple_val(TCAR(l))[1]) == pos)
                    l1 = CONS(hp, TCAR(l), l1);
                else
                    l2 = CONS(hp, TCAR(l), l2);
                hp += 2;
                l = TCDR(l);
            }
            if (l1 != NIL && TCDR(l1) == NIL)
                l1 = TCAR(l1);
            if (l2 != NIL && TCDR(l2) == NIL)
                l2 = TCAR(l2);
            // l1 和 l2 插入到插槽中
            ARRAY_PUT(pd, pos, l1);
            ARRAY_PUT(pd, pos + homeSize, l2);
        }
    }
}

tab中的数据为进程字典data数组的长度可能的取值

static unsigned int tab[] = {
    10UL,
    20UL,
    40UL,
    80UL,
    160UL,
    320UL,
    640UL,
    1280UL,
    2560UL,
    5120UL,
    10240UL,
    20480UL,
    40960UL,
    81920UL,
    163840UL,
    327680UL,
    655360UL,
    1310720UL,
    2621440UL,
    5242880UL,
    10485760UL,
    20971520UL,
    41943040UL,
    83886080UL,
    167772160UL,
    335544320UL,
    671088640UL,
    1342177280UL,
    2684354560UL
}

总结:
进程初始化时,进程字典可用插槽数为8。当插槽被使用完毕后,设 x = p->dictionary->usedSlots+(p->dictionary->usedSlots / 4) & 0xf,然后从tab数组中找到最小的大于等于x的值,设该值为y,然后扩充进程字典 data字段的长度为y,同时设置可以使用插槽数usedSlots字段为x。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值