Golang Map发生扩容之后Bucket如何进行迁移

109 阅读4分钟

Go的map迁移过程是渐进式迁移,其实这样做的目的就是为了防止大面积的迁移导致的性能问题。如果如果一次性进行全部迁移有可能会发生系能抖动的情况

Go的渐进式迁移是怎么做的呢?look:
一般情况下,读写删的时候都有可能触发渐进式库容。go map底层中,每次迁移只会迁移2个bucket。

// t map的类型信息
// h 当前hmap
// bucket 当前需要操作的key所在的桶
func growWork(t *maptype, h *hmap, bucket uintptr) {
    // make sure we evacuate the oldbucket corresponding
    // to the bucket we're about to use
    // 迁移函数evacuate
    evacuate(t, h, bucket&h.oldbucketmask())

    // evacuate one more oldbucket to make progress on growing
    if h.growing() {
       evacuate(t, h, h.nevacuate)
    }
}

bucket&h.oldbucketmask() 这个方法相当于h.oldbucketmask() == 1<<(B-1) - 1。这里是用来定位到oldbucket的位置。

其实这里我是有疑问的。如果是增量扩容的况下B是+1的。因为key通过hash算法之后的到一个64位的无符号整数。这个时候就出现一个问题,没有扩容之前取的是后B位确定桶,高8位确定在桶的那个位置(cell)。但是发生扩容之后B+1了,桶的位置发生了变化。然后要进行迁移,这个时候要怎么去确定当前key在老桶的位置呢?

那就看看吧:

假设key,通过hash算法算出的值为:
十进制:0xc404f767de07a98c
二进制:1100010000000100111101110110011111011110000001111010100110001100
oldbucket长度:2^3 = 8,B = 3 ,0 - 7
newbucket长度:2^4 = 16,B = 4,0 - 15

桶掩码:
oldbucketmask = 2^(B-1) = 7 = 0b0111
newbucketmask = 2^(B-1) = 15 = 0b1111
其实有点类似桶的下标index从0开始计算

这个时候定位桶的位置,取后B位就是100然后与oldbucketmash做&运算,计算出4号桶

0b0111 = oldbucketmash
0b0100 = key的后B位
------
0b0100

0b0100 = 4

这个时候发生增量扩容的情况,在查询到这个key的时候,发现他在老桶上并没有迁移到新桶上。这个时候就开始进行渐进式迁移。首先底层代码会判断在新桶上还是老桶上(这个后边说)。因为发生了扩容,B的值肯定是会变化的,这个时候要怎么去老桶上找呢?look:

  • 定位key在老桶的位置
  • 确定后B位置,因为扩容是翻倍的,所以只需要进行2^(B-1)
  • 然后就是上面的运算过程,就知道在4号桶中
  • 但是这个时候我们要迁移了
扩容后B = 4
所以我们要去后4位去计算当前key在新桶的位置,后四位为 1100
0b1111 = newbucketmash
0b1100 = key的后4位
------
ob1100 = 12
这个时候就可以迁移到第12个桶中了

另一种情况:
假设一个hash出来的值为hash = 0b10110

hash = 0b10110

B = 3 取hash后3位
在老桶中为:
0b0111 = oldbucketmash
0b0110 = hash
------
0b0110 = 6 在桶6中

扩容迁移
B = 4 取hash后40b1111 = newbucketmash
0b0110 = hash
------
0b0110 = 6 在桶6中 

其实还有一个算法确定新桶的位置
0b1000 = 1 << 3(oldB) = 8
0b0110 = hahs 
------
0b0000
从左往右数,从0开始,第oldB位:
如果为0那就是key要迁过去的新桶的位置和老桶的桶号是一样
如果是1那就是key所在新桶的桶号为:X+2^B = 12 也就是6 + 8 = 14
X对应的就是新桶和旧桶的位置,因为是翻倍扩容的所以1:2一个旧桶对应两个新桶
新桶 A = X
新桶 B = X + (1 << oldB)
根据以上的公式:X = 6 ,(1 << oldB) = 8 = 2^3
所以就等于12

在举个例子:
shah = 0b11110
oldB = 3 = 8 bucket
newB = 4 = 16 bucket

旧桶的位置:
oldbcket = shah & ((1 << 3) -1)

((1 << 3) -1) = 0b111
shah = 0b11110

0b111
0b110
-------
0b110  对用就是6号桶和直接取shah的后B位是一样

发生扩容:
B = 4
newbit = (1 << 3) = 0b1000  
hash = 0b11110

0b1000
0b1110
-------
0b1000 = newbit 这里从右往左数ondB位从0开始也就是1所以根据
X + (1 << oldB) = 6 + 8 = 12
12就是这个key所在桶的桶号

查找hash = 0b11110
现在B = 4
取后B位置:1110 = 12

以上为个人理解,可能有错的地方希望大佬们可以指出来,谢谢各位。