Go map的實現(三)map 的數據訪問


本文在golang map 數據結構的基礎上,學習map 數據是如何訪問的。

map 創建示例

在golang 中,訪問 map 的方式有兩種,例子如下:


val := example1Map[key1]
val, ok := example1Map[key1]

第一種方式不判斷是否存在key值,直接返回val (可能是空值)
第二種方式會返回一個bool 值,判斷是否存在key 鍵值。(是不是和redis 的空值判斷很類似)

那訪問map 時,底層做了什么,我們一起來探究

對於不同的訪問方式,會使用不同的方法,下面是內部提供的幾種方法,我們一起來學習:


// 迭代器中使用
func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer){}

// 不返回 bool
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {}
func mapaccess1_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) unsafe.Pointer {}

// 返回 bool
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {}
func mapaccess2_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) (unsafe.Pointer, bool) {}

這些方法有很大的相關性,下面我們逐一來學習吧。

mapaccess1_fat, mapaccess2_fat

這兩個方法,從字面上來看多了個fat,就是個寬數據。何以為寬,我們從下面代碼找到原因:

//src/cmd/compile/internal/gc/walk.go

if w := t.Elem().Width; w <= 1024 { // 1024 must match runtime/map.go:maxZero
  n = mkcall1(mapfn(mapaccess1[fast], t), types.NewPtr(t.Elem()), init, typename(t), map_, key)
} else {
  z := zeroaddr(w)
  n = mkcall1(mapfn("mapaccess1_fat", t), types.NewPtr(t.Elem()), init, typename(t), map_, key, z)
}

這是構建語法樹時,mapaccess1 相關的代碼(mapaccess2_fat 也類似), 如果val 大於1024byte 的寬度,那會調用fat 后綴的方法。
原因是,在map.go 文件中,定義了val 0值的數組,代碼如下:

const maxZero = 1024 // must match value in cmd/compile/internal/gc/walk.go
var zeroVal [maxZero]byte

但是這個零值只能對寬度小於1024byte的寬度的數據有效,所以對於返回值(val)寬度小於1024 的,直接調用mapaccess1 方法即可,否則需要首先找一個對應的0值數據,然后調用mapaccess1_fat 方法,如果為0,傳出對應的0值數據。

mapaccess1, mapaccess2

mapaccess1 與 mapaccess2 的差別在於是否返回返回值,mapaccess2 將返回bool 類型作為是否不存在相應key的標識,mapaccess1 不會。所以,這里着重分析mapaccess2. 代碼如下:

func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {
  // 竟態分析 && 內存掃描
  // ...

  if h == nil || h.count == 0 {
    // map 為空,或者size 為 0, 直接返回
  }
  if h.flags&hashWriting != 0 {
    // 這里會檢查是否在寫,如果在寫直接panic
    throw("concurrent map read and map write")
  }

  // 拿到對應key 的hash,以及 bucket
  alg := t.key.alg
  hash := alg.hash(key, uintptr(h.hash0))
  m := bucketMask(h.B)
  b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
  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)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize)))
    if !evacuated(oldb) {
      b = oldb
    }
  }

  // 獲取tophash 值
  top := tophash(hash)
bucketloop:
  // 遍歷解決沖突的鏈表
  for ; b != nil; b = b.overflow(t) {
    // 遍歷每個bucket 上的kv
    for i := uintptr(0); i < bucketCnt; i++ {
      // 先匹配 tophash
      // ...

      // 獲取k
      k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
      if t.indirectkey() {
        k = *((*unsafe.Pointer)(k))
      }

      // 判斷k是否相等,如果相等直接返回,否則繼續遍歷
      if alg.equal(key, k) {
        v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
        if t.indirectvalue() {
          v = *((*unsafe.Pointer)(v))
        }
        return v, true
      }
    }
  }
  return unsafe.Pointer(&zeroVal[0]), false
}

訪問map的流程比較簡單:

  • 首先,獲取key 的hash值,並取到相應的bucket
  • 其次,遍歷對應的bucket,以及bucket 的鏈表(沖突鏈)
  • 對於每個bucket 需要先匹配tophash 數組中的值,如果不匹配,則直接過濾。
  • 如果hash 匹配成功,還是需要匹配key 是否相等,相等就返回,不等繼續遍歷。

這里需要注意一點:在tophash 數組中不僅會標識是否匹配hash值,還會標識下個數組中是否還有元素,減少匹配的次數。代碼如下:

if b.tophash[i] != top {
  if b.tophash[i] == emptyRest {
    break bucketloop
  }
  continue
}

tophash 的值有多種情況, 如果小於minTopHash,則作為標記使用。下面是標識含義:

  emptyRest      = 0 // 標記為空,且后面沒有數據了 (包括overflow 和 index)
  emptyOne       = 1 // 在被刪除的時候設置為空
  evacuatedX     = 2 // kv 數據被遷移到新hash表的 x 位置
  evacuatedY     = 3 // kv 數據被遷移到新hash表的 y 位置
  evacuatedEmpty = 4 // bucket 被轉移走了,數據是空的
  minTopHash     = 5 // 閾值標識

enptyRest 是有利於數據遍歷的,減少了對數據的訪問次數
evacuateX 和 evacuateY 與數據遷移有關,我們在賦值部分學習(賦值才有可能遷移)

總結

  • map 中,val 如果寬度比較大,0值問題也需要多分配內存。所以,這種情況,使用指針肯定是合理的。(當然,內存拷貝也是一個問題)
  • tophash 值的含義參考第一篇 bucket章節

今天的作業就交完了。下一篇將學習golang 賦值的實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM