go源码分析——map(全网最详细)

map

一、简单结构

map类型的变量本质上是一个hmap类型的指针,键值对通过哈希表来储存。如下图:

go源码分析——map(全网最详细)_第1张图片

二、哈希表的设计

1、结构

hash表首先有一段储存储存数据的连续空间,我们将数据存到这段连续的空间(可以把它当组成一个数组)里面,如下图所示,把key、value以及key的hash值存到连续的空间里面

go源码分析——map(全网最详细)_第2张图片

2、寻址

然后就是当前的key-value具体要存到哪一个位置呢?现在主流的通常有两种方法:

1、取模运算:hash % m

2、位运算:hash&(m-1)

我们都知道位运算肯定是更加的高效,但是必须得限制m为2的整数幂。golang里面采用了幂运算的方法来寻址

3、hash冲突

当我们两个key发生hash冲突了怎么处理呢?我们也介绍两种的方法

1、拉链法:这种方法会把发生hash冲突的key-value数据用链表的方式把数据组织起来。如下图所示,当发生hash冲突的时候,后面插入的数据会链接到发生冲突的数据后面

go源码分析——map(全网最详细)_第3张图片
2、开放地址法:比如下图,我们编号0的位置已经被占了,那么我们就把数据放在编号1的位置,如果编号1的位置也被占了,由此类推,我们继续寻址,直到找到空闲的地方。

当我们去查找数据的时候,我们先会查找0位置,如果不是我们查找的key,会继续向下查找,知道查找到最后的位置,很显然,这种方法不适合负载因子(后面会介绍)过大的情况

go源码分析——map(全网最详细)_第4张图片

上面两种方法对比:拉链法相对于查找数据比较友好,查找的次数不会太多。开放地址法则不会产生不连续的内存空间,每次申请的都是一块连续的内存空间。在golang中,采用了类似于拉链法,为什么说类似呢?后面会详细分析一下map的数据结构。

4、扩容

当我们的数据已经远远超过8个,采用拉链法,那么hash表就会退化成链表。开发地址法就更不行了。这时候我们就会涉及到扩容的问题。扩容我们主要搞清楚两个问题就行了:何时扩容 、怎么扩容

何时扩容

还记得我们上文提到的负载因子吗?负载因子就是来标识何时扩容的一个参数。它是哈希表中存储的键值对数量和桶数量的比值,通常需要设定一个触发扩容的最大负载因子

loadFactor = keyCount / bucketCount

我们假设我们的负载因子是50%,当我们来了一个新的key12的时候,就会触发扩容

go源码分析——map(全网最详细)_第5张图片
怎么扩容

我们直接的想法是,当达到负载因子的时候,直接把buckets的容量翻倍。然后把数据都copy到新的地址空间去。如下所示

go源码分析——map(全网最详细)_第6张图片
这么迁移会有个问题。当旧桶的数据非常多的时候呢?这时候迁移数据就会占用大量时间,影响我们主程序。所以几乎在所有的语言中,都会采用渐进式扩容

这种方式下,旧桶里的键值对是分多次挪到新桶里的,可以在触发扩容后,先分配新桶,然后标记当前哈希表正在扩容,并在哈希表的读写操作中判断若在扩容中,则迁移一部分键值对到新桶里,这样可以把扩容消耗的时间分散到多次操作中。

go源码分析——map(全网最详细)_第7张图片
好了,上面我们大概介绍了hash表的一些特性和问题,接下来我们再介绍一下golang中的map长什么样子。

三、map的数据结构

图解结构

map底层的哈希表通过与运算的方式选择桶,所以hmap中并不直接记录桶的个数,而是记录这个数目是2的多少次幂。下面我们先看看map使用的桶长什么样子。我们先看看之前的map基本结构图,知道golang中一个桶其实就是一个bmap。

go源码分析——map(全网最详细)_第8张图片
下图是一个bmap的数据结构。我们可以看到bmap其实是一段连续内存空间。

go源码分析——map(全网最详细)_第9张图片
为了内存使用更加紧凑,8个键放一起,8个值放一起。对应每个key只保存其哈希值的高8位(tophash)。而每个键值对的tophash、key和value的索引顺序一一对应。

可以看到这个其实和我们上面说的拉链法比较相似,如果发生hash冲突的时候,会在bmap中向后写入,这样既保证了查找次数较少,又能保证内存比较连续。

8个存满了怎么办?为了减少扩容次数,这里引入了溢出桶(overflow)。溢出桶的内存布局与常规桶相同。如果哈希表要分配的桶的数目大于2^4,就会预分配2^(B-4)个溢出桶备用。这些常规桶和溢出桶在内存中是连续的,只是前2^B个用作常规桶,后面的用作溢出桶。

源码分析

下面我们具体来看看源码:

type hmap struct {
  count     int //map中值的数量
  flags     uint8 //动作标识(比如正在写数据)
  B         uint8  // 常规桶个数等于2^B
  noverflow uint16 // 溢出桶数量
  hash0     uint32 // hash 种子
  
  buckets    unsafe.Pointer //桶的指针,指向的是 2^B 个bmap
  oldbuckets unsafe.Pointer //旧桶的指针(扩容时使用)
  nevacuate  uintptr //扩容时迁移的位置       

  extra *mapextra // 所有溢出桶指针
}


type mapextra struct {
    overflow    *[]*bmap //已经用到的溢出桶
    oldoverflow *[]*bmap //渐进式扩容时,保存旧桶用到的溢出桶
    nextOverflow *bmap   //下一个尚未使用的溢出桶
}
type bmap struct {
  tophash [bucketCnt]uint8 //存储tophash的值
}

可以看到如果当前桶存满了以后,检查hmap.extra.nextoverflow还有可用的溢出桶,就在这个桶后面链上这个溢出桶,然后继续往这个溢出桶里

go源码分析——map(全网最详细)_第10张图片

四、创建map

我们知道创建map一般用make(map)的方式,下面我们看看创建map具体是怎么操作的,下图是创建map时的主流程:

  1. 根据需要初始化的大小来确定B 的值
  2. 如果B 的值≥4 ,确定溢出桶的数量
  3. 分配桶和溢出桶的内存

go源码分析——map(全网最详细)_第11张图片
源码分析:

// make map返回的是指针
func makemap(t *maptype, hint int, h *hmap) *hmap {
  if h == nil {
    h = new(hmap)
  }
  h.hash0 = fastrand()
  // 根据hint初始化桶的大小
  B := uint8(0)
  for overLoadFactor(hint, B) {
    B++
  }
  h.B = B

  // 如果桶的数量大于0,初始化桶的内存
  if h.B != 0 {
    var nextOverflow *bmap
    h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
    if nextOverflow != nil {
      h.extra = new(mapextra)
      h.extra.nextOverflow = nextOverflow
    }
  }
  return h
}

//给map bucket分配内存
//  @param t map的类型
//  @param b 桶的个数2^b
//  @param dirtyalloc 是否要把返回的buckets指向dirtyalloc地址
//  @return buckets buckets地址
//  @return nextOverflow 溢出桶地址
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
  base := bucketShift(b)
  nbuckets := base //bucket的数量
  
  //当b大于4是,添加溢出桶,溢出桶的大小为 2^(b-4)个
  if b >= 4 {
    nbuckets += bucketShift(b - 4)
  }

   //申请桶的空间
  if dirtyalloc == nil {
    buckets = newarray(t.bucket, int(nbuckets))
  } else {
    buckets = dirtyalloc
    size := t.bucket.size * nbuckets
    if t.bucket.ptrdata != 0 {
      memclrHasPointers(buckets, size)
    } else {
      memclrNoHeapPointers(buckets, size)
    }
  }

 //有溢出桶,nextOverflow指向溢出桶的地址
  if base != nbuckets {
    nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
    last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
    last.setoverflow(t, (*bmap)(buckets))
  }
  return buckets, nextOverflow
}

五、通过key获取val

当我们使用val,ok := map[key]会发生什么呢?同样,我们先通过一个流程图了解一下主流程:

  1. 判断map是否为空
  2. 判断是否有协程在并发的写、
  3. 定位到key对应的桶位置,如果map正在扩(等量)容且该桶还未被迁移,定位到旧桶的位置
  4. 遍历该桶的每个tophash、key,直到找到和指定key相等的数据

go源码分析——map(全网最详细)_第12张图片
下面我们看看源码:

//  @param t map的类型
//  @param h map数据
//通过key获取val
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
   //map为nil或者没有数据,返回零值
  if h == nil || h.count == 0 {
    if t.hashMightPanic() {
      t.hasher(key, 0) 
    }
    return unsafe.Pointer(&zeroVal[0])
  }

  //@Step1 有并发的协程正在写,抛出异常(panic)
  if h.flags&hashWriting != 0 {
    throw("concurrent map read and map write")
  }

  //@Step2 计算key的hash,定位到当前key的hash桶位置
  hash := t.hasher(key, uintptr(h.hash0))
  m := bucketMask(h.B)
  b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))

  //@Step3 判断有没有旧桶(正在迁移)
  //  如果旧桶数据没有被迁移,定位到当前key在旧桶的位置
  //  如果当前旧桶没有被迁移,则迁移桶
  if c := h.oldbuckets; c != nil {
    if !h.sameSizeGrow() { //不是进行同长度的扩容(当map被删除过多时,需要重新整合数据,避免浪费过多空间)
      m >>= 1
    }
    oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
    if !evacuated(oldb) {
      b = oldb
    }
  }

  //取top hash
  top := tophash(hash)

  //@Step4 从bucket中找出key对应的val,循环遍历top hash
bucketloop:
  for ; b != nil; b = b.overflow(t) { //是否有溢出桶
    for i := uintptr(0); i < bucketCnt; i++ {
      //top hash是否相等
      if b.tophash[i] != top {
        if b.tophash[i] == emptyRest { //如果top hash为emptyRest,则表示后面没数据了
          break bucketloop
        }
        continue
      }
      k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
      if t.indirectkey() {
        k = *((*unsafe.Pointer)(k))
      }
      // key是否相等,相等则返回对应的val
      if t.key.equal(key, k) {
        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])//返回零值
}

六、添加key value

添加key-val的时候发生了什么呢?同样,我们先来看一下流程图:

  1. 判断map是否为空、判断有没有协程并发写
  2. 如果buckets为空,申请一个长度为1的buckets
  3. 找出改key对应的桶位置
  4. 如果map正在迁移切该桶没有被迁移,迁移该桶
  5. 遍历该桶,如果找到相同的key,返回val的位置。如果没有找到,找出下一个空位置,赋值tophash、key,返回val的位置
  6. 判断map是否需要扩容,如果扩容,返回5的操作
  7. 如果当前buckets和溢出buckets都没有位置了,添加一个溢出buckets,赋值tophash、key到第一个空位,返回val的位置

go源码分析——map(全网最详细)_第13张图片
源码分析:

// 新增或者替换key val  m[key]=val
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  //@Step1 如果map为nil,panic
  if h == nil {
    panic(plainError("assignment to entry in nil map"))
  }

  // 判断有没有协程正在写map
  if h.flags&hashWriting != 0 {
    throw("concurrent map writes")
  }
  hash := t.hasher(key, uintptr(h.hash0))

  // 设置map写的标志
  h.flags ^= hashWriting

  //@Step2 buckets为nil,new一个
  if h.buckets == nil {
    h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
  }

again:
  //@Step3 找出key hash对应的桶
  bucket := hash & bucketMask(h.B)
  if h.growing() {
    //  @Step4 如果桶需要迁移,则把旧桶迁移
    growWork(t, h, bucket)
  }
  b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
  top := tophash(hash)

  var inserti *uint8
  var insertk unsafe.Pointer
  var elem unsafe.Pointer
  //@Step5 寻找map中有没有存在的key
  //  5-1 如果存在,更新key的值,则返回val的位置
  //  5-2 如果不存在,则记录bucket最近一个空位置的tophash 、key、value的位置
  //  5-2-1 判断bucket有没有溢出,如果没有溢出,则下一步。
  //  5-2-2 溢出了则找出下一个溢出桶,继续bucketloop上述操作
bucketloop:
  for {
    for i := uintptr(0); i < bucketCnt; i++ {
      if b.tophash[i] != top {
        //判断当前元素是否为空,如果为空,记录第一个为空的位置(方便找不到key时插入)
        if isEmpty(b.tophash[i]) && inserti == nil {
          inserti = &b.tophash[i]
          insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
          elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
        }
        if b.tophash[i] == emptyRest {
          break bucketloop
        }
        continue
      }
      k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
      if !t.key.equal(key, k) {
        continue
      }
      elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
      //找到了key
      goto done
    }
    //在常规桶中没有找到数据,在溢出桶继续找
    ovf := b.overflow(t)
    if ovf == nil {
      break
    }
    b = ovf
  }

  //@Step6 如果添加一个元素会造成bucket超过负载(6.5),或者溢出bucket太多
  //  扩充桶,返回上面逻辑bucketloop继续寻找val位置
  if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
    hashGrow(t, h)
    goto again 
  }

  //@Step7 当前bucket和溢出桶都满了,重新添加一个溢出桶
  if inserti == nil {
    newb := h.newoverflow(t, b)
    inserti = &newb.tophash[0]
    insertk = add(unsafe.Pointer(newb), dataOffset)
    elem = add(insertk, bucketCnt*uintptr(t.keysize))
  }

  // 储存key、tophash的位置。h.count +1
  if t.indirectkey() {
    kmem := newobject(t.key)
    *(*unsafe.Pointer)(insertk) = kmem
    insertk = kmem
  }
  if t.indirectelem() {
    vmem := newobject(t.elem)
    *(*unsafe.Pointer)(elem) = vmem
  }
  typedmemmove(t.key, insertk, key)
  *inserti = top
  h.count++

  // 返回val的位置
done:
  if h.flags&hashWriting == 0 {
    throw("concurrent map writes")
  }
  h.flags &^= hashWriting
  if t.indirectelem() {
    elem = *((*unsafe.Pointer)(elem))
  }
  return elem
}

七、删除key value

删除key的时候发生了什么呢?同样,我们先来看一下流程图:

go源码分析——map(全网最详细)_第14张图片
源码分析:

// 删除key、val
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
  //判断map是否处于写状态
  if h.flags&hashWriting != 0 {
    throw("concurrent map writes")
  }

  hash := t.hasher(key, uintptr(h.hash0))
  //把map置为写状态
  h.flags ^= hashWriting

  //1 找出对应的桶位置
  //  如果需要迁移,继续迁移
  bucket := hash & bucketMask(h.B)
  if h.growing() {
    growWork(t, h, bucket)
  }
  b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
  bOrig := b
  top := tophash(hash)

  //2
  //  2-1 找出tophash key val位置
  //  2-2 把tophash置为 emptyOne(当前位置为空,但后面还有元素)
  //  2-3 当前bucket后面没有元素,则置为emptyRest(当前位置为空,且后面没有元素)
search:
  for ; b != nil; b = b.overflow(t) {
    for i := uintptr(0); i < bucketCnt; i++ {
      if b.tophash[i] != top {
        if b.tophash[i] == emptyRest { //如果发现对应的tophash已经是emptyRest状态,则标识后面没有数据了
          break search
        }
        continue
      }
      k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
      k2 := k
      if !t.key.equal(key, k2) {
        continue
      }
      e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
      //清除val
      if t.indirectelem() {
        *(*unsafe.Pointer)(e) = nil
      } else if t.elem.ptrdata != 0 {
        memclrHasPointers(e, t.elem.size)
      } else {
        memclrNoHeapPointers(e, t.elem.size)
      }
      // 把tophash置为 emptyOne(当前位置为空,但后面还有元素)
      b.tophash[i] = emptyOne
      //3 判断下一个位置(可能是溢出桶第一个)tophash是不是emptyRest
      //  3-1 如果不是,直接到notLast结束流程
      //  3-2 如果是,往前搜索,把所有emptyOne置为emptyRest
      if i == bucketCnt-1 {
        if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
          goto notLast
        }
      } else {
        if b.tophash[i+1] != emptyRest {
          goto notLast
        }
      }
      for {
        b.tophash[i] = emptyRest
        if i == 0 {
          if b == bOrig {
            break 
          }
          c := b
          for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
          }
          i = bucketCnt - 1
        } else {
          i--
        }
        if b.tophash[i] != emptyOne {
          break
        }
      }
      
    notLast:
      //数量减1
      h.count--
      break search
    }
  }
}

六、迭代key value

遍历key-val就比较简单了。给定一个随机桶位置startBucket,和随机桶内数据位置offset,开始遍历map的每个值:

go源码分析——map(全网最详细)_第15张图片
源码分析:

// 初始化迭代器
func mapiterinit(t *maptype, h *hmap, it *hiter) {
  //1 初始化迭代器
  it.t = t
  if h == nil || h.count == 0 {
    return
  }
  it.h = h
  it.B = h.B
  it.buckets = h.buckets

  //2 给一个随机数,决定迭代桶的开始位置和桶内开始迭代的顺序
  r := uintptr(fastrand())
  if h.B > 31-bucketCntBits {
    r += uintptr(fastrand()) << 31
  }
  it.startBucket = r & bucketMask(h.B)          //桶的偏移量
  it.offset = uint8(r >> h.B & (bucketCnt - 1)) //桶内偏移量

  it.bucket = it.startBucket
}

//迭代器迭代
//  1 从随机偏移桶循环迭代每个桶数据
//  2 桶内从随机偏移量开始遍历
//  3 列出该方法隐掉了当正在进行扩容时怎么迭代,需要了解参考源码
func mapiternext(it *hiter) {

  if h.flags&hashWriting != 0 { //有协程正在写
    throw("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 { //当前桶指针为nil,标识桶内数据已经遍历完成,需要遍历下一个桶
    if bucket == it.startBucket && it.wrapped { //已经遍历到开始的桶
      it.key = nil
      it.elem = nil
      return
    }
    bucket++
    if bucket == bucketShift(it.B) {
      bucket = 0
      it.wrapped = true
    }
    i = 0
  }
  for ; i < bucketCnt; i++ {
    offi := (i + it.offset) & (bucketCnt - 1) //从桶内哪个位置开始遍历
    if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {//没数据
      continue
    }
    k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
    e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
  b = b.overflow(t)
  i = 0
  goto next
}


参考:

https://github.com/golang/go/...

你可能感兴趣的:(go后端)