5.go map源码-查找/迭代

367 阅读4分钟

上集回顾:map扩容

1.查找

在go map代码中,查找部分的逻辑应该是最简单的,唯一值得重视的一点就是,key所在的桶是新桶还是旧桶。按照2倍扩容的规则,查找旧桶则需要 B=B/2B = B/2

1.1 校验和定位桶

查找部分同样需要做系列校验判断,需要注意一点是,在定位桶的位置时,需要旧桶有没有存在数据还没迁移,存在则是查找旧桶。

/*
:params t: 存储了key/value和桶大小等信息
:params h: map
:params key: key值
*/
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    if raceenabled && h != nil {
        callerpc := getcallerpc()
        pc := abi.FuncPCABIInternal(mapaccess1)
        racereadpc(unsafe.Pointer(h), callerpc, pc)
        raceReadObjectPC(t.key, key, callerpc, pc)
    }
    if msanenabled && h != nil {
        msanread(key, t.key.size)
    }
    if asanenabled && h != nil {
        asanread(key, t.key.size)
    }

    // 未初始化或者没有元素,直接返回
    if h == nil || h.count == 0 {
        if t.hashMightPanic() {
            t.hasher(key, 0) // see issue 23734
        }
        return unsafe.Pointer(&zeroVal[0])
    }
    // map不允许并发读写,会触发该panic,这里是通过flags的标记位来判断是否正在进行写操作
    if h.flags&hashWriting != 0 {
        fatal("concurrent map read and map write")
    }
    // 确定桶位置
    hash := t.hasher(key, uintptr(h.hash0))
    m := bucketMask(h.B)
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
    // 检查检查旧桶有没有数据,有就要先处理旧桶,由于新桶总是旧桶的2倍,需要将对数-1
    if c := h.oldbuckets; c != nil {
        // 找到旧桶对应的索引地址
        if !h.sameSizeGrow() {
            // There used to be half as many buckets; mask down one more power of two.
            m >>= 1
        }
        oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
        if !evacuated(oldb) {
            b = oldb
        }
    }
    // tophash即首8位(高8位),用于快速比较。
    top := tophash(hash)
    
    ...

2.查找key值

查找key值的逻辑相对简单,同样是遍历链桶和桶内数据,通过hash高8位和key值判断,来最终确认key的位置,返回对应的value值。

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    ...
    
bucketloop:
    // 遍历链桶查找
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                // 遍历到最后一个元素,还是没找到退出循环
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            // hash高8位相同,需要进一步判断key值是否一致
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            if t.key.equal(key, k) {
                // key值一致就返回对应value值
                e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                if t.indirectelem() {
                    e = *((*unsafe.Pointer)(e))
                }
                return e
            }
        }
    }
    return unsafe.Pointer(&zeroVal[0])
}

2.迭代

2.1 初始化迭代器

在初始化的时候,有一点是比较有趣的,每次的迭代的开始位置都是不一样的,从里面能找到答案,源码中是直接随机选择了开始迭代的hash数组的位置。

在思考作者为什么这样做的时候,我们回想一下在go map的设计中,除了人为控制迭代开始位置以外,还有哪些因素会影响到迭代的不确定性:

  1. 哈希种子,在初始化/空元素会对map的哈希种子进行重置(哈希种子会影响到对应的哈希值,即:桶位置)
  2. 扩容,在扩容期间,所有的元素虽然有规律进行迁移,但是元素位置已经是被打乱了。

在整个map的元素位置的不确定性中,如果只对map进行迭代(没有其他行为),会让用户误以为map内部的元素是有序的。所以作者加了最后一把锁,相同的map每次迭代开始的桶位置都是随机的。

主要控制随机的字段

  • it.startBucket:这个是hash数组的偏移量,表示遍历从这个桶开始。
  • it.offset:这个是桶内的偏移量,表示每个桶的遍历都从这个偏移量开始。

遍历过程:

  1. 从hash数组中第it.startBucket个桶开始,先遍历hash桶,然后是这个桶的溢出链。
  2. 之后hash数组偏移量+1,继续前一步动作。
  3. 遍历每一个桶,无论是hash桶还是溢出桶,都从it.offset偏移量开始。如果只是随机一个开始的桶,range结果还是有序的;但每个桶都加it.offset偏移,这个输出结果就有点扑朔迷离,大家可以亲手试下,对同一个map多次range)
  4. 当迭代器经过一轮循环回到it.startBucket的位置,结束遍历。
/*
:params t: 存储了key/value和桶大小等信息
:params h: map
:params it: 迭代器
*/
func mapiterinit(t *maptype, h *hmap, it *hiter) {
    if raceenabled && h != nil {
        callerpc := getcallerpc()
        racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapiterinit))
    }

    it.t = t
    if h == nil || h.count == 0 {
        return
    }

    if unsafe.Sizeof(hiter{})/goarch.PtrSize != 12 {
        throw("hash_iter size incorrect") // see cmd/compile/internal/reflectdata/reflect.go
    }
    it.h = h

    // 获取桶状态的快照
    it.B = h.B
    it.buckets = h.buckets
    if t.bucket.ptrdata == 0 {
        // 分配当前切片并记住指向当前切片和旧切片的指针。这将保持所有相关的溢出桶处于活跃状态。
	// 即使在迭代时表增长和溢出桶添加到表中
        h.createOverflow()
        it.overflow = h.extra.overflow
        it.oldoverflow = h.extra.oldoverflow
    }

    // 随机定位:根据随机数,选择一个桶位置作为起始点进行遍历迭代
    var r uintptr
    if h.B > 31-bucketCntBits {
        r = uintptr(fastrand64())
    } else {
        r = uintptr(fastrand())
    }
    // 这个是hash数组的偏移量,表示遍历从这个桶开始
    it.startBucket = r & bucketMask(h.B)
    // 这个是同桶内的偏移量,表示每个桶的遍历都是从这个偏移量开始
    it.offset = uint8(r >> h.B & (bucketCnt - 1))

    // 迭代器状态
    it.bucket = it.startBucket

    //记住我们有一个迭代器。可以与另一个mapiteinit()同时运行。
    if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
        atomic.Or8(&h.flags, iterator|oldIterator)
    }

    mapiternext(it)
}

2.2 迭代hash数组的桶

2.2和2.3章节本来是合并的,但想着分开看的话,逻辑会显的更简单易懂。

这章主要针对准备下一个桶位置进行确认

  1. 遍历到最后,没有后续桶了,就结束迭代
  2. 存在,就需要检查是迭代新桶还是旧桶,并找到对应的桶位置

回顾一下几个重要的参数 iter结构体

参数含义
it.startBucket开始的桶
it.offset每个桶开始的偏移量
it.bptr当前遍历的桶
it.iit.bptr已经遍历的键值对数量,i初始化为0,当i=8时表示这个桶遍历完了,将it.bptr移向下一个桶
it.key每次迭代的key结果
it.value每次迭代的value结果
it.wrapped已经从hash数组的尾部到头部了,通俗点就是:hash数组我已经全部遍历过了
/*
:params it: 迭代器
*/
func mapiternext(it *hiter) {
    h := it.h
    // 校验
    if raceenabled {
        callerpc := getcallerpc()
        racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapiternext))
    }
    if h.flags&hashWriting != 0 {
        fatal("concurrent map iteration and map write")
    }
    // 元素赋值
    t := it.t
    bucket := it.bucket
    b := it.bptr
    i := it.i
    checkBucket := it.checkBucket

next:
    if b == nil {
        // 结束迭代
        if bucket == it.startBucket && it.wrapped {
            it.key = nil
            it.elem = nil
            return
        }
        // 下一个桶遍历前检查(主要是遍历新桶还是旧桶处理)
        if h.growing() && it.B == h.B {
            // 迭代器是在扩容中启动的,当扩容尚未完成。如果我们当前桶尚未填充(即旧存储桶尚未迁移)
            // 那么我们需要遍历旧桶,只返回将要迁移到当前桶的桶
            oldbucket := bucket & it.h.oldbucketmask()
            b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
            // 这里检查了该桶有没有已经迁移完毕,没就需要遍历该桶
            if !evacuated(b) {
                checkBucket = bucket
            } else {
                b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
                checkBucket = noCheck
            }
        } else {
            b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
            checkBucket = noCheck
        }
        bucket++
        // 已经从 hash 数组的结尾到开头了
        if bucket == bucketShift(it.B) {
            bucket = 0
            it.wrapped = true
        }
        i = 0
    }
   
   ...
}

2.3 迭代当前链桶

这一部分的代码之所以苦涩难懂,主要在扩容期间启动的迭代器,但迁移还没全部完成(需要做兼容)。 具体方案细节可以查看:迭代细节

带着疑问从代码中找答案


func mapiternext(it *hiter) {
    ...
    
    // 桶内遍历
    for ; i < bucketCnt; i++ {
       // 确认随机开始位置(桶内需要随机开始位置)
        offi := (i + it.offset) & (bucketCnt - 1)
        // 嗯...因为随机开始位置的原因,isEmpty标志只能作为跳过处理,不能作为结束标志(旧桶同理)
        if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
            continue
        }
        // 计算key/value对应的值(因为实际上是一段连续空间,key/vlaue位置是逻辑位置,需要计算得到)
        k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
        if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
        }
        e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
        if checkBucket != noCheck && !h.sameSizeGrow() {
            // 特殊情况:迭代器在扩容期间启动, 但扩容尚未完成
            // 我们正在处理一个桶,它的旧桶还没有被迁移。或者至少,当我们启动桶的时候,它还没有被迁移
	    // 因此,我们迭代旧存储桶,跳过将会到另一个新存储桶的任何key(旧桶扩容为2倍)
            if t.reflexivekey() || t.key.equal(k, k) {
                // 如果旧存储桶中的item不是迭代中当前新存储桶的目标,请跳过它。
                hash := t.hasher(k, uintptr(h.hash0))
                if hash&bucketMask(it.B) != checkBucket {
                    continue
                }
            } else {
                // 如果 k != k(NaNs),哈希不可重复。
                // 我们需要一个可重复的随机选择,在迁移过程中向那个方向发送NaN。我们将使用 tophash的低位来决定
                // 注意:这种情况就是为什么我们需要两个 evacuatedX和evacuatedY的evacuated tophash值,它们的低位不同。
                if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
                    continue
                }
            }
        }
        
        // 这是可用的数据(还没被迁移),或者 key ==key,可以返回它
        if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
            !(t.reflexivekey() || t.key.equal(k, k)) {
            it.key = k
            if t.indirectelem() {
                e = *((*unsafe.Pointer)(e))
            }
            it.elem = e
        } else {
            // 自迭代器启动以来,哈希表一直在扩容(迁移)。这个可用的数据对应的key现在已经发生了变化。
            // 需要检查当前哈希表中的数据。
            // 这段代码处理 key已被删除/更新或删除并重新插入的情况。
            // 注意:我们需要重新获取key,因为它可能已经被更新为一个 equal(),但密钥不同。
            rk, re := mapaccessK(t, h, k)
            if rk == nil {
                continue
            }
            it.key = rk
            it.elem = re
        }
        it.bucket = bucket
        if it.bptr != b { // 避免不必要的写障碍; see issue 14921
            it.bptr = b
        }
        it.i = i + 1
        it.checkBucket = checkBucket
        return
    }
    b = b.overflow(t)
    i = 0
    goto next
}