关于map和slice预分配内存补充|青训营笔记

582 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

之前在课上有讲到map和slice预分配可以节省内存,当时听得比较迷糊没懂什么意思,课后查找了一些,留在这里作为些许补充。

slice预分配内存

我们都知道slice实际上只是一个对底层数组的描述,包含指向底层数组的指针。因此,对slice进行append,实际是对底层数组进行的操作。slice有三个属性,指针(ptr)、长度(len) 和容量(cap)。当我们使用append()对已有的slice添加元素时,如果添加新元素之后的长度小于等于cap(len+newparam<=cap),会在原底层数组剩余的空间进行插入。如果append新元素之后的长度大于 cap ,则会另外分配一片更大的区域,把原数组整个copy过去,然后再进行元素插入,并删除原来的数组。

每次新扩大空间,一般会扩大为原数组的两倍大小,很显然,如果原数组相对比较大的时候,新扩大的数组会有较多的空间浪费。如果我们在前期就能大致估计最后的切片长度,指定slice大小进行内存预分配,就可以节省一定的空间。

map预分配内存

我们先来看一下map初始化的源码,其中hint就是指定容量大小的参数。

func makemap(t *maptype, hint int, h *hmap) *hmap {
	if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) {
        //超过最大可分配内存就直接不分配了
		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
                //此处调用makeBucketArray函数分配bucket
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

正如我们所知,map底层使用hash表实现,就像所有的哈希算法一样需要一个hash table和bucket桶。 留意上面我标出来的地方———当 hint 大于等于8时,在初始化map过程中,就会通过调用makeBucketArray函数分配内存给bucket。如果我们不指定内存容量,就由go智能分配。

很显然,在初次分配之后,在对map的使用过程中,当我们存放大量键值对的时候,go原本分配的内存不够,就需要再次分配内存用作bucket(这里省略了一个溢出桶的小优化没有提),进行扩容迁移。这些频繁的操作会增加程序开销,造成性能的下降。扩容的过程毕竟是自动的,也会造成一定的空缺浪费。因此我们在初始化map时对所需空间进行指定,可以一定程度上提高性能。