Slice提前分配内存
golang的slice不仅有长度,还有容量的概念。在append的时候,slice如果扩容,内存会重新分配。频繁重新分配内存是有消耗的,通常情况下,可以直接使用make([]byte, 0, len(list))的方式提前分配内存。
但是一些特定的业务场景,需要对多个slice进行拼接,且数据源slice数量大小不确定,有的还会涉及并发。比如要获取30天的数据进行处理,开启了30个协程获取每一天的数据,最后append拼接成一个slice进行处理。
如果不提前分配内存,实现思路大致如下,为了并发安全,需要在append的时候使用锁
func getMonthRecord() []byte {
var result []byte
wg := sync.WaitGroup{}
appendLock := sync.Mutex{}
for i := 0; i < 30; i++ {
wg.Add(1)
go func() {
defer wg.Done()
currentList := getDayRecord()
appendLock.Lock()
result = append(result, currentList...)
appendLock.Unlock()
}()
}
wg.Wait()
return result
}
如果要提前分配内存,代码如下。不仅不需要多次分配内存,而且固定长度的dayRecordList可以直接赋值,可以不需要使用锁。
func getMonthRecord() []byte {
dayRecordList := make([][]byte, 30)
wg := sync.WaitGroup{}
for i := 0; i < 30; i++ {
wg.Add(1)
go func(ind int) {
defer wg.Done()
dayRecordList[ind] = getDayRecord()
}(i)
}
wg.Wait()
return aggregateRecord(dayRecordList)
}
// 分配内存并拼接slice
func aggregateRecord(dayRecordList [][]byte) []byte {
var totalNum int
for _, list := range dayRecordList {
totalNum += len(list)
}
result := make([]byte, 0, totalNum)
for _, list := range dayRecordList {
result = append(result, list...)
}
return result
}
为了验证对性能的影响,简单实现getDayRecord如下,执行次数为1000。但是由于time.Sleep的影响,减去10s后,很尴尬地发现并没有什么差别,于是去掉time.Sleep。没有提前分配内存,为63ms左右。提前分配内存,为25ms左右
func getDayRecord() []byte {
time.Sleep(10 * time.Millisecond)
return make([]byte, 1000)
}
使用更加简单粗暴的方法进行验证。循环10000次的差距更加明显,不提前分配内存151ms,提前分配内存31ms,差了几乎4倍数
dayList := make([][]byte, 0, 30)
for i := 0; i < 30; i++ {
dayList = append(dayList, getDayRecord())
}
// 不提前分配内存
for i := 0; i < 10000; i++ {
var result []byte
for _, list := range dayList {
result = append(result, list...)
}
}
// 提前分配内存
for i := 0; i < 10000; i++ {
var totalNum int
for _, list := range dayList {
totalNum += len(list)
}
result := make([]byte, 0, totalNum)
for _, list := range dayList {
result = append(result, list...)
}
}
多个slice的拼接,计算完总长度后提前分配内存,虽然代码多了些,但是并不没有增加多少理解难度,在效率上也是更高的。还是建议使用提前分配内存的写法