【Go源码】map实现

 

 

Go 语言map实现采用的是哈希查找表,并且使用链表解决哈希冲突(数组+链表)。

map数据结构

type hmap struct {
	count     int
	flags     uint8
	B         uint8
	noverflow uint16
	hash0     uint32
	buckets    unsafe.Pointer
	oldbuckets unsafe.Pointer
	nevacuate  uintptr
	extra *mapextra
}

属性解释

  • count 表示当前哈希表中的元素数量;
  • flags是并发读写标志;
  • noverflow是溢出桶数量;
  • B 是 buckets 数组的长度的对数,也就是说 buckets 数组的长度就是 2^B。
  • hash0 是哈希的种子,它能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入;
  • oldbuckets 是哈希在扩容时用于保存之前 buckets 的字段,它的大小是当前 buckets 的一半;

buckets 是一个指针,最终它指向的是一个结构体:

type bmap struct {
	tophash [bucketCnt]uint8
}

但这只是表面(src/runtime/hashmap.go)的结构,编译期间会给它加料,动态地创建一个新的结构:

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

bmap 就是我们常说的“桶”,桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是“一类”的。在桶内,又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。

来一个整体的图:

 

 

当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。

type mapextra struct {
	// overflow[0] contains overflow buckets for hmap.buckets.
	// overflow[1] contains overflow buckets for hmap.oldbuckets.
	overflow [2]*[]*bmap

	// nextOverflow 包含空闲的 overflow bucket,这是预分配的 bucket
	nextOverflow *bmap
}

bmap 是存放 k-v 的地方,我们把视角拉近,仔细看 bmap 的内部组成。

bmap struct

上图就是 bucket 的内存模型,HOB Hash 指的就是 top hash。 注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding 字段,节省内存空间。

例如,有这样一个类型的 map:

map[int64]int8

如果按照 key/value/key/value/... 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 key/key/.../value/value/...,则只需要在最后添加 padding。

每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个 bucket ,通过 overflow 指针连接起来。

 

map 创建

创建map时主要使用如下函数

func makemap_small() *hmap 
func makemap(t *maptype, hint int, h *hmap) *hmap 
func makemap64(t *maptype, hint int64, h *hmap) *hmap // hint类型为int64, 实质还是调用的 makemap
当创建map时不指定hint大小,如下面所示的m1。那么调用makemap_small来进行创建。

当指定了hint(代表初始化时可以保存的元素的个数)的大小的时候,若hint<=8, 使用makemap_small进行创建map,否则使用makemap创建map。

    m1 := make(map[string]string)
    m2 := make(map[string]string, hint)

makemap_small 源码分析

主要是创建hmap结构并初始化hash因子就结束了,并没有初始化buckets,makemap的要较其复杂一些,下面将结合具体的例子进行说明

func makemap_small() *hmap {
    h := new(hmap) 
    h.hash0 = fastrand()
    return h
}

makemap源码分析- make(map[string]string, 10)

下面进行源码分析(64位cpu中指针占8个字节),并对关键的变量值以及步骤进行说明。
从上面的分析可以知道,创建 make(map[string]string, 10) ,由于hint=10, 大于8,因此将使用makemap来实现。

//  hint=10,可以容纳hint个元素
func makemap(t *maptype, hint int, h *hmap) *hmap {
    if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) {
        hint = 0
    } 

    // initialize Hmap
    if h == nil {
        h = new(hmap)
    }
    h.hash0 = fastrand() // hash因子

    // 确定B的大小,每个桶(不含溢出桶)可以有8个k/v对,hmap中含有 1<< B 个桶,具体见overLoadFactor分析
    B := uint8(0)
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B // 此时 B=1

    // h.B = 1 创建buckets
    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
}

overLoadFactor 实现
在确定hmap.B的值的时候,需要调用此函数。当调用make(map[string]string, 10)时,count=1。

const (
    bucketCntBits = 3
    bucketCnt     = 1 << bucketCntBits // =8

    loadFactorNum = 13
    loadFactorDen = 2
)    

// 如果 count > 8 && count > 13 * ( (1<<B) / 2 ), 返回true
// 1 << B bucket个数, 负载因子为: 13/2=6.5
func overLoadFactor(count int, B uint8) bool {
    return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

makeBucketArray
确定桶的个数后,进行内存的分配,内存的分配采用array连续内存的分配方式。

// b=1
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
    base := bucketShift(b) // base = 1 << 1 = 2
    nbuckets := base       // nbuckets = 2

    if b >= 4 {
        nbuckets += bucketShift(b - 4)
        sz := t.bucket.size * nbuckets
        up := roundupsize(sz)
        if up != sz {
            nbuckets = up / t.bucket.size
        }
    }

    if dirtyalloc == nil {
        // 申请内存,结构为一个数组,每个元素为 bucket, 个数为 1<<B = 2个,会申请连续内存大小为 bucket.size*nbuckets = 2*272 = 544个字节
        // 这里说明一下 bucket.size为什么等于272? bmap的结构由四个部分组成,tophash,8个key,8个value,1一个指针。
        // tophash是一个数组,数组的大小为8,类型为uint8, uint8占一个字节,总计字节 8*1 = 8
        // key,value的数据类型都是string类型,string类型占16个字节,总计字节 8*16 + 8*16 = 256
        // 指针在64位cpu上占8个字节。因此总和为 8 + 256 + 8 = 272 个字节
        buckets = newarray(t.bucket, int(nbuckets)) 
    } else {
        buckets = dirtyalloc
        size := t.bucket.size * nbuckets
        if t.bucket.kind&kindNoPointers == 0 {
            memclrHasPointers(buckets, size)
        } else {
            memclrNoHeapPointers(buckets, size)
        }
    }

    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
}

  

map Get

暂留。
 
 
 
refer:
https://www.ctolib.com/docs/sfile/go-internals/02.3.html
https://www.cnblogs.com/linkstar/p/10969631.html
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
https://github.com/qcrao/Go-Questions/tree/master/map
posted @ 2020-04-18 14:45  -零  阅读(612)  评论(0编辑  收藏  举报