Golang之map tophash详解
tophash是一个长度为8的数组,它不仅仅用来存放key的哈希高8位,在不同场景下它还可以标记迁移状态,bucket是否为空等。弄懂tophash对我们深入了解map实现非常重要。
如上图:每一个tophash唯一对应一个K/V对。
tophash值含义
当tophash对应的K/V被使用时,存的是key的哈希值的高8位;当tophash对应的K/V未被使用时,存的是K/V对应位置的状态。
下面是map源码中对tophash状态值的定义。
@(src/runtime/map.go)
emptyRest = 0
emptyOne = 1
evacuatedX = 2
evacuatedY = 3
evacuatedEmpty = 4
minTopHash = 5
当tophash[i] < 5时,表示存的是状态;
当tophash[i] >= 5时,表示存的是哈希值;
那么问题来了,如果key的哈希值高8位小于minTopHash时,这时候怎么区分是存的状态还是哈希值?
func tophash(hash uintptr) uint8 {
top := uint8(hash >> (sys.PtrSize*8 - 8))
if top < minTopHash {
top += minTopHash
}
return top
}
直接看上面源码,第3-5行可以知道,当计算的哈希值小于minTopHash时,会直接在原有哈希值基础上加上minTopHash,确保哈希值一定大于minTopHash。
emptyRest
这个值有两层意思:一是表示该tophash对应的K/V位置是可用的;二是表示该位置后面的K/V位置都是可用的。
@(伪代码)
if tophash[i] == emptyRest {
1 keys[i] is empty && values[i] is empty
2 keys[i+1 .. N-1] is empty && values[i+1 .. N-1] is empty
}
赋值:
- 初始化的时,tophash会被置为emptyRest。
- 删除map元素时,会判断是否需要把删除key对应的tophash置为emptyRest。
作用:
-
判断bucket是否为空
当tophash[0]==emptyRest表示整个bucket都是空的,这就是源码里面判断bucket是否为空的方法。 -
查找时快速判断后面位置是否还需遍历
如在查找时,在一个bucket中,找到tophash[2]位置,发现值为emptyRest,就可以判断该bucket没有该元素,继续查找下一个bucket。
emptyOne
仅表示该tophash对应的K/V位置是可用的,其后面的是否可用不知道。
赋值:
删除map元素时,会把key对应的tophash先置为emptyOne,再继续判断是否需要置为emptyRest。
evacuatedX && evacuatedY
这两个状态与扩容有关,记录元素被迁移到了新桶的部位–X或Y。如果是等位迁移,旧桶的元素必然被迁移到X部;如果是扩容迁移,旧桶元素可能迁移到X部,也可能迁移到Y部。当迁移到X部时,旧桶tophash置为evacuatedX;当迁移到Y部时,旧桶tophash置为evacuatedY。如下图:
[外链图片转存失败(img-hm1qcNAM-1567759485577)(./1560947852018.png)]
举个例子说明:扩容迁移,要把旧桶1的元素迁到新桶,因为新桶长度增长了一倍,因此旧桶1元素可能被迁移到新桶的1或5。当元素迁移到了1时,把旧桶tophash置为evacuatedX;反之,迁移到了5时,tophash置为evacuatedY。要注意置的是旧桶的tophash。
旧桶迁移完就被回收了,为啥还要记录每个元素迁移的位置?
想了解原因,我们必须要考虑一个很复杂的场景:遍历map时,开始扩容。map遍历并不是原子操作,在遍历过程中会有数据插入、删除等,会导致map扩容。因为遍历发生在扩容前,因此一直是遍历老桶。这时有两种情况:老桶数据没有迁移,这时直接从老桶返回K/V就可以了;老桶数据已经迁移,这时就需要重新查找map。那怎么判断老桶数据是否迁移?这时就用到evacuatedX和evacuatedY,这两个就是用来标记老桶数据迁移状态的。
在map.go的941~966行:
if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) || !(t.reflexivekey() || alg.equal(k, k)) {
it.key = k
if t.indirectvalue() {
v = *((*unsafe.Pointer)(v))
}
it.value = v
} else {
rk, rv := mapaccessK(t, h, k)
if rk == nil {
continue // key has been deleted
}
it.key = rk
it.value = rv
}
evacuatedEmpty
当bucket被迁移完时,tophash值置为evacuatedEmpty。