进程字典的结构:
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。