Golang map实现分析

数据结构

go的map采用数组+链表形式存储,数据存放于hmap中:

type hmap struct {
    count     int // 哈希表的元素个数,即len()
    flags     uint8  // map状态
    B         uint8  // 2^B为桶的数量
    noverflow uint16 // 溢出桶的数量(预估)
    hash0     uint32 // hash seed

    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    oldbuckets unsafe.Pointer // 扩容时用于保存之前的buckets字段
    nevacuate  uintptr        // 下一个迁移的桶编号

    extra *mapextra // 溢出桶
}

type mapextra struct {
    // overflow和oldoverflow保证所有溢出桶的存活,不被gc回收
    overflow    *[]*bmap    // 存放的所有溢出桶的地址
    oldoverflow *[]*bmap    // 存放的所有老的溢出桶的地址

    nextOverflow *bmap      // 指向的下一个溢出桶的地址
}

在buckets中存放的是bmap

// A bucket for a Go map.
type bmap struct {
    tophash [bucketCnt]uint8    // bucketCnt=8
}

//编译期间数据结构
type bmap struct{
    topbits     [8]uint8     //用于表示标志位或hash值高八位来快速定位K/V位置
    keys        [8]keytype
    value       [8]valuetype
    overflow    uintptr    //连接下个bmap溢出桶
}

bmap中仅包含了tophash字段,通过比较不同键的哈希的高8位可以减少访问键值对次数以提高性能。bmap在编译时确定K/V的类型,会存储有key、value、溢出桶信息。每个bmap中最多只会有8个元素,超出部分回连接溢出桶进行存储。

源码分析

初始化

// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap {
	// 判断申请内存是否超过限制
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	if overflow || mem > maxAlloc {
		hint = 0
	}

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

	// 如果hint大于8并且hint大于(1<<b)*6.5 就每次增长1
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// allocate initial hash table
	// if B == 0, the buckets field is allocated lazily later (in mapassign)
	if h.B != 0 {
		var nextOverflow *bmap
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)  // 初始化buckets和溢出桶
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
	return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

扩容

hmap的扩容会在两种情况下触发:

  • 元素过多:hmap中的元素大于8个并且元素数量大于6.5 * (2 ^B)(见overLoadFactor函数),此时会进行翻倍扩容,即hmap.B+=1
  • 桶过多:B <= 15时,溢出桶的数量大于等于(2 ^ B)B > 15时,溢出桶的数量大于等于(2 ^ 15)(见tooManyOverflowBuckets函数),此时会进行等量扩容,将桶中的元素重新排列,以缩减溢出桶的数量

hmap采用渐进式扩容方式,可以避免一次性扩容带来的性能瞬时抖动。当老桶中的数据还没被迁移时,get就会从老桶中获取。

sync.Map

适合读多更新多但新增少的场景,结构体如下:

// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
type Map struct {
    mu Mutex		// 互斥锁

    // 只读数据,无需加锁
    read atomic.Value // readOnly

    // 访问需要加锁,新添加的key都会先放到dirty中
    dirty map[interface{}]*entry

    // 统计访问read没有未命中然后穿透访问dirty的次数
    misses int
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
    // If p == nil, the entry has been deleted, and either m.dirty == nil or m.dirty[key] is e.
    //
    // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry is missing from m.dirty.
    //
    // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty != nil, in m.dirty[key].
    p unsafe.Pointer // *interface{}
}

sync.Map会把读和写的数据分离存储,读取数据时先尝试无锁从read中读取,dirty会记录新增/删除的数据,以减少对read的修改,对于更新操作也有多处使用CAS操作以减少lock操作(见Store方法)。

参考:
go语言之map源码分析
Go 语言设计与实现——哈希表

posted @ 2024-01-23 20:47  及第  阅读(7)  评论(0编辑  收藏  举报