代码:
package main
import (
"fmt"
"time"
)
func useAppend(n int) {
var a []int
for i :=0 ;i
输出:
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。