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。

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