golang爬坑笔记之自问自答系列(5)——切片赋值避免使用append()函数

代码:

package main

import (
	"fmt"
	"time"
)

func useAppend(n int)  {
	var a []int
	for i :=0 ;i<n;i++{
		a = append(a,i)
	}
}
func initGiveLen(n int)  {
	var a = make([]int,n)
	for i:=0;i<n;i++ {
		a[i]=i
	}
}

func main() {
	N :=10000000
	start := time.Now().UnixNano()
	useAppend(N)
	endAppend := time.Now().UnixNano()-start
	fmt.Println("append耗时(ns):",endAppend)

	start= time.Now().UnixNano()
	initGiveLen(N)
	endGivenLen := time.Now().UnixNano()-start
	fmt.Println("初始化切片长度耗时(ns):",endGivenLen)

	fmt.Printf("append是初始化长度耗时的%d倍",endAppend/endGivenLen)
}

输出:

append耗时(ns): 229515000
初始化切片长度耗时(ns): 6085000
append是初始化长度耗时的37倍

问题:为何进行同样的切片赋值工作,append()函数时间消耗会是初始化切片长度的数十倍?

答:在回答该问题的时候,我想贴上两段源代码,以下是内置的append函数源代码。

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//	slice = append(slice, elem1, elem2)
//	slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
//	slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

其二runtime包下slice.go的一段源代码。(至于我是如何找到这个函数和append会有关联的,这是在使用pprof做cpu性能分析时,发现runtime.growslice()函数的占比很高。关于pprof,推荐一个学习地址https://github.com/google/pprof)

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {}

上面两端代码的意思是啥呢?当切片使用append函数时,会同时调用runtime.growslice()函数。当旧切片的容量cap不能满足装下新增的数据时,它分配新的底层数组,并将旧切片的数据拷贝到新的切片(切片地址没有变,只是切片指针发生变化)。需要注意的是,新切片的容量是以2的倍数动态调整的,看以下代码:

package main

import "fmt"

func main() {
	var a []int
	for i:=0;i<20;i++ {
		a = append(a,i)
		fmt.Printf("len:%d,cap:%d\n",len(a),cap(a))
	}
}

输出:

len:1,cap:1
len:2,cap:2
len:3,cap:4
len:4,cap:4
len:5,cap:8
len:6,cap:8
len:7,cap:8
len:8,cap:8
len:9,cap:16
len:10,cap:16
len:11,cap:16
len:12,cap:16
len:13,cap:16
len:14,cap:16
len:15,cap:16
len:16,cap:16
len:17,cap:32
len:18,cap:32
len:19,cap:32
len:20,cap:32

所以,可以看到,当采用append函数为切片添加数据时,它会反复建立新的切片,带来很多额外开销,因此运行效率自然就比一开始初始化就通过make()指定大小的切片低很多。

Tip:在能确定待添加切片的数据长度时,使用var X = make(type,len())的形式初始化。只有在无法确定待添加数据长度时,才考虑使用append。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章