go map
Caution
源码方面看的是不支持泛型的版本. 未完待续…
1 图书馆与map
| map | 图书馆对应 |
|---|---|
| 桶 | 一个个分类不同的书架 |
| 哈希算法 | 你根据书名进行判断的判断方法,知道是哪个分类, 放到哪个书架上 |
| 哈希冲突 | 多本书,根据书名进行判断,都在同一个分类书架上,就依次放到书架上的空余位置即可 如果该书架满了,则需要新的同分类名的书架,将书放到这个新的书架上. |
| 读取数据 | 你根据书名进行判断,知道是哪个分类书架,然后到该书架上 从左到右比对过去,比对书名(ISBN),找到并拿走 |
| 写入数据 | 你根据书名进行判断,知道是哪个分类书架,然后到该书架上 从左到右比对过去,比对书名(ISBN),如果有,则更换书,没有就放入最右边空余位置 |
| 装载因子 (元素个数/桶数量<=6.5) |
很显然书架上的书越多,你找书就越慢, 如果每个书架上都只有一个本书, 那么你一定非常快就找到了. |
2 源码分析
2.1 数据结构
Tip
go tool compile -S main.go 查看汇编代码可以看到 runtime.makemap
容量小的话,可以看到runtime.makemap_small
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
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
}src/runtime/map.go
type hmap struct {
// map 中的元素个数,必须放在 struct 的第一个位置,
// 因为内置的 len 函数会通过unsafe.Pointer会从这里读取
count int
flags uint8
// bucket的数量是2^B, 最多可以放 loadFactor * 2^B 个元素,再多就要 hashGrow 了
// 装载因子 loadFactor= 元素个数/桶数量<=6.5
B uint8
//overflow 的 bucket 近似数
noverflow uint16
hash0 uint32 // hash seed
//2^B 大小的数组指针,如果 count == 0 的话,可能是 nil
buckets unsafe.Pointer
// 扩容的时候,buckets 长度会是 oldbuckets 的两倍,只有在 growing 时候为空。
oldbuckets unsafe.Pointer
// 指示扩容进度,小于此地址的 buckets 迁移完成
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
// 当 key 和 value 都可以 inline 的时候,就会用这个字段
extra *mapextra // optional fields
}上面这个数据结构并不是 golang runtime 时的结构,在编译时候编译器会给它动态创建一个新的结构
因为哈希表中可能存储不同类型的键值对,而且 Go 语言也不支持泛型,所以键值对占据的内存空间大小只能在编译时进行推导
- 为什么 桶内的key和value 要这样设计? 我们以
map[int64]int8为例来说明- 如果按照 key/value/key/value/… 这样的模式存储,由于内存对齐,在每一个 key/value 之后都要额外需要7个字节
- int64 8个字节 int8一个字节, 剩下的7个字节就浪费了. 因为接下来的key 8个字节你放不下
- 而将所有的 key,value 分别绑定到一起,这种形式 key/key/…/value/value/…,则只需要在最后添加 pad
- 每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个 bucket ,通过 overflow 指针连接起来
2.2 B值的计算
2.2.1 源码解析
// 判断是否超过了装载因子
func overLoadFactor(count int, B uint8) bool {
// bucketCntBits = 3
// bucketCnt = 1 << bucketCntBits //1 左移3位 = 8
// 得出 bucketCnt = 8
// loadFactorNum = 13
// loadFactorDen = 2
// count>bucketCnt 如果容量> 一个桶能放的元素数
// 且 第一次循环, B=0, 10>13*(1/2) 一个桶最多6.5 ,我们装载因为是6.5,超过了,说明需要扩容桶
// 则第二循环, B=1, bucketShift(B)= 1<<1=2 , 10>13*(2/2) 不城里,退出循环了, 结果是B=1
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}2.2.2 代码验证
package main
import (
"fmt"
"unsafe"
)
type MapStruct struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets uintptr //unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra uintptr
}
func main() {
m := make(map[int32]int32, 10)
// m 实际是个指针, 指向了真正的hmap
m[1] = 22
m[2] = 33
fmt.Println(len(m)) //2
hmapReal := *(**MapStruct)(unsafe.Pointer(&m))
fmt.Println(hmapReal.count, hmapReal.B) // 2 1
bks := hmapReal.buckets
for i := 0; i < (1 << hmapReal.B); i++ {
// bmap的size=88
kbase := bks + uintptr(i*88)
for j := 0; j < 8; j++ {
fmt.Println("key=", *(*int32)((unsafe.Pointer)(kbase + uintptr(8+j*4))))
fmt.Println("val=", *(*int32)((unsafe.Pointer)(kbase + uintptr(8+8*4+j*4))))
}
}
}2.3 访问
2.3.1 for range 为啥是随机的
package main
import (
"fmt"
)
func main() {
m := make(map[int32]int32, 10)
m[1] = 22
m[2] = 33
m[3] = 44
m[4] = 55
m[5] = 66
for k, v := range m {
fmt.Println(k, v)
}
}查看汇编 可以看到 写入map 的相关函数runtime.mapassign_fast32
range 的相关函数 runtime.mapiterinit
mapiterinit()
2.4 扩容
2.4.1 为什么需要扩容
桶里key 多起来了,8个满了, 再overflow 整个几个. 你查询起来就慢了. 需要遍历桶里的元素, 这就是有装载因子的判断