zoukankan      html  css  js  c++  java
  • 【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
  • 相关阅读:
    移动端前端开发调试
    Safari 前端开发调试 iOS 完美解决方案
    IOS下移除按钮原生样式 -webkit-appearance
    修复iPhone的safari浏览器上submit按钮圆角bug
    解决 placeholder 垂直不居中,偏上的问题
    如何使用JavaScript和正则表达式进行数据验证
    关于VSS(Volume Shadow Copy Service)一
    centOS目录结构
    如何解决windows 80端口被占用的情况
    linux系统TCP协议之Send(转)
  • 原文地址:https://www.cnblogs.com/-wenli/p/12725871.html
Copyright © 2011-2022 走看看