map 创建

源码位置 src/runtime/map.go

初始化 map 的两种方式:

make(map[int]int)
make(map[int]int, hint) // hint是初始化map的hint, 即桶的大小

对于不指定初始化大小, 和初始化值 hint < 8 时, go会调用 make_small 函数, 并直接从堆上进行分配.

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

当 hint>8 时, 则调用 makemap 函数.

makemap

// 如果编译器认为map和第一个bucket可以直接创建在栈上, h和bucket可能都是非空
// 如果h != nil, 那么map可以直接在h中创建.
// 如果h.buckets != nil, 那么h指向的bucket可以作为map的第一个bucket使用.
func makemap(t *maptype, hint int, h *hmap) *hmap {
    // math.MulUintptr返回hint与t.bucket.size的乘积, 并判断该乘积是否溢出.
    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    // maxAlloc的值, 根据平台系统的差异而不同,具体计算方式参照src/runtime/malloc.go
    if overflow || mem > maxAlloc {
        hint = 0
    }

    // initialize Hmap
    if h == nil {
        h = new(hmap)
    }
    // 通过fastrand得到哈希种子
    h.hash0 = fastrand()

    // 根据输入的元素个数hint, 找到能装下这些元素的B值
    B := uint8(0)
    //  hint > 8 && uintptr(hint) > bucketShift(B)*6.5
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B

    // 分配初始哈希表
    // 如果B为0,那么buckets字段后续会在mapassign方法中lazily分配
    if h.B != 0 {
        var nextOverflow *bmap
        // makeBucketArray创建一个map的底层保存buckets的数组,它最少会分配h.B^2的大小。
        h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
        if nextOverflow != nil {
            h.extra = new(mapextra)
            h.extra.nextOverflow = nextOverflow
        }
    }
    return h
}

makeBucketArray

// makeBucket为map创建用于保存buckets的数组. 
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
    base := bucketShift(b)
    nbuckets := base

    // 对于小的b值(小于4),即桶的数量小于16时,使用溢出桶的可能性很小. 对于此情况, 就避免计算开销. 
    if b >= 4 {
        // 当桶的数量大于等于16个时, 正常情况下就会额外创建2^(b-4)个溢出桶
        nbuckets += bucketShift(b - 4)
        sz := t.bucket.size * nbuckets // 计算内存大小
        up := roundupsize(sz)          // 计算mallocgc将分配的内存块的大小(需要以此为准)
        if up != sz {
            nbuckets = up / t.bucket.size
        }
    }

    // 这里, dirtyalloc 分两种情况. 如果它为nil, 则会分配一个新的底层数组. 
    // 如果它不为nil,则它指向的是曾经分配过的底层数组, 该底层数组是由之前同样的t和b参数通过makeBucketArray分配的,
    // 如果数组不为空,需要把该数组之前的数据清空并复用. 
    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) // 最终都会调用此方法
        }
    }

    // 即b大于等于4的情况下, 会预分配一些溢出桶. 
    // 为了把跟踪这些溢出桶的开销降至最低, 使用了以下约定:
    // 如果预分配的溢出桶的overflow指针为nil, 那么可以通过指针碰撞(bumping the pointer)获得更多可用桶. 
    // (关于指针碰撞: 假设内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的
    // 指示器, 那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离, 这种分配方式称为"指针碰撞")
    // 对于最后一个溢出桶, 需要一个安全的非nil指针指向它. 
    if base != nbuckets {
        // buckets(基地址) + base(2^B)*bucketsize, 即获得第一个 overflow
        nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
        // 最后一个 overflow
        last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
        last.setoverflow(t, (*bmap)(buckets)) // 最后一个 overflow 指针指向 buckets(基地址, 也是安全的指针)
    }
    return buckets, nextOverflow
}

注: 上述的 makeBucketArray 代码当前可以得到的一个结论是, 正常状况下, 正常的桶和溢出的桶在内存存储空间是连续的, 只是被 hmap 中的不同的字段引用而已.

相关辅助函数

// 地址偏移(基于内存必须连续)
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
  return unsafe.Pointer(uintptr(p) + x)
}

// 判断B和count是否满足map条件
func overLoadFactor(count int, B uint8) bool {
  // loadFactorNum=13, loadFactorDen=2
  return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

// 2^b, b的有效范围[0-63]
func bucketShift(b uint8) uintptr {
  // sys.PtrSize 是系统的指针占用字节大小, 32位系统4, 64位系统是8
  return uintptr(1) <8 - 1))
}

goframe map根据Removes 删除_字段