代碼:
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。