前言
這兩篇文章分別介紹了從make、array/slice切片構造slice的具體底層處理過程,本文則介紹通過append生成新的slice的過程。
Slice append
append func
// 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
以上爲builtin/builtin.go中關於append func的說明。append會返回一個新的slice,因此必須保存append的結果。
我們知道,append會追加一個或多個數據至slice中,這些數據會存儲在slice的持有的數組中。
數組的長度是固定的,意味着存儲的數據是有限的。剩餘空間足以容納追加的數據,則可以正常將數據存入數組。一旦追加數據後總長度超過數組長度後,則原數組已無法存儲新數據。那要怎麼處理呢?
runtime/slice.go中只有擴容的growslice func,其調用主要在cmd/compile/internal/gc/walk.go中,處理相對複雜。我們可以先看下refelct/value.go下的Append源碼,此部分的處理過程很完整和簡單。
reflect Append
// Append appends the values x to a slice s and returns the resulting slice.
// As in Go, each x's value must be assignable to the slice's element type.
func Append(s Value, x ...Value) Value {
s.mustBe(Slice)
s, i0, i1 := grow(s, len(x))
for i, j := i0, 0; i < i1; i, j = i+1, j+1 {
s.Index(i).Set(x[j])
}
return s
}
// grow grows the slice s so that it can hold extra more values, allocating
// more capacity if needed. It also returns the old and new slice lengths.
func grow(s Value, extra int) (Value, int, int) {
i0 := s.Len()
i1 := i0 + extra
if i1 < i0 {
panic("reflect.Append: slice overflow")
}
m := s.Cap()
if i1 <= m {
return s.Slice(0, i1), i0, i1
}
if m == 0 {
m = extra
} else {
for m < i1 {
if i0 < 1024 {
m += m
} else {
m += m / 4
}
}
}
t := MakeSlice(s.Type(), i1, m)
Copy(t, s)
return t, i0, i1
}
Append處理過程如下:
- 判斷當前slice長度i0與追加數據的總長度i1是否溢出,溢出則報錯;
- 若i1小於/等於slice的cap(底層數組的長度),直接返回原slice的起始及結束數據部分
- 否則,當前底層數組已無法存儲所有的追加數據,需要進行擴容處理:
-
若當前cap爲0,則直接已追加數據的長度爲新cap;
-
若i1大於slice的cap m,開始逐步擴容cap,直至大於總數據總長i1
- 若原數據長度i0<1024,則m翻倍;
- 否則,m自增1/4
-
構建新的Slice
-
將原slice的數據拷貝至新slice中,並返回新slice。
- 將追加的數據存入指定的位置中
append
append的具體調用處理在cmd/compile/internal/gc/walk.go中,核心處理代碼如下:
// Node ops.
const (
OXXX Op = iota
...
OAPPEND // append(List); after walk, Left may contain elem type descriptor
...
)
...
...
case OAPPEND:
// x = append(...)
r := n.Right
if r.Type.Elem().NotInHeap() {
yyerror("%v is go:notinheap; heap allocationdisallowed", r.Type.Elem())
}
switch {
case isAppendOfMake(r):
// x = append(y, make([]T, y)...)
r = extendslice(r, init)
case r.IsDDD():
r = appendslice(r, init) // also works for appen(slice, string).
default:
r = walkappend(r, init, n)
}
n.Right = r
if r.Op == OAPPEND {
// Left in place for back end.
// Do not add a new write barrier.
// Set up address of type for back end.
r.Left = typename(r.Type.Elem())
break opswitch
}
// Otherwise, lowered for race detector.
// Treat as ordinary assignment.
}
...
可以看到針對append的具體處理分爲3種情況:
- extendslice
針對格式如下:
append(x , make([]T, y)...)
針對帶有make初始化的append處理
2. appendslice
針對格式如下:
append(l1, l2...)
針對append slice處理
3. walkappend
針對格式如下:
append(l1, l2...)
針對append多個具體的元素處理
三者的處理過程稍有差異,此處以appendslice爲例來說下具體的處理過程,其他的處理方式如有興趣,可以自行去查看下。
appendslice
// expand append(l1, l2...) to
// init {
// s := l1
// n := len(s) + len(l2)
// // Compare as uint so growslice can panic on overflow.
// if uint(n) > uint(cap(s)) {
// s = growslice(s, n)
// }
// s = s[:n]
// memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T))
// }
// s
//
// l2 is allowed to be a string.
func appendslice(n *Node, init *Nodes) *Node {
walkAppendArgs(n, init)
l1 := n.List.First()
l2 := n.List.Second()
var nodes Nodes
// var s []T
s := temp(l1.Type)
nodes.Append(nod(OAS, s, l1)) // s = l1
elemtype := s.Type.Elem()
// n := len(s) + len(l2)
nn := temp(types.Types[TINT])
nodes.Append(nod(OAS, nn, nod(OADD, nod(OLEN, s, nil), nod(OLEN, l2, nil))))
// if uint(n) > uint(cap(s))
nif := nod(OIF, nil, nil)
nuint := conv(nn, types.Types[TUINT])
scapuint := conv(nod(OCAP, s, nil), types.Types[TUINT])
nif.Left = nod(OGT, nuint, scapuint)
// instantiate growslice(typ *type, []any, int) []any
fn := syslook("growslice")
fn = substArgTypes(fn, elemtype, elemtype)
// s = growslice(T, s, n)
nif.Nbody.Set1(nod(OAS, s, mkcall1(fn, s.Type, &nif.Ninit, typename(elemtype), s, nn)))
nodes.Append(nif)
// s = s[:n]
nt := nod(OSLICE, s, nil)
nt.SetSliceBounds(nil, nn, nil)
nt.SetBounded(true)
nodes.Append(nod(OAS, s, nt))
var ncopy *Node
if elemtype.HasHeapPointer() {
// copy(s[len(l1):], l2)
nptr1 := nod(OSLICE, s, nil)
nptr1.SetSliceBounds(nod(OLEN, l1, nil), nil, nil)
nptr2 := l2
Curfn.Func.setWBPos(n.Pos)
// instantiate typedslicecopy(typ *type, dst any, src any) int
fn := syslook("typedslicecopy")
fn = substArgTypes(fn, l1.Type, l2.Type)
ncopy = mkcall1(fn, types.Types[TINT], &nodes, typename(elemtype), nptr1, nptr2)
} else if instrumenting && !compiling_runtime {
// rely on runtime to instrument copy.
// copy(s[len(l1):], l2)
nptr1 := nod(OSLICE, s, nil)
nptr1.SetSliceBounds(nod(OLEN, l1, nil), nil, nil)
nptr2 := l2
if l2.Type.IsString() {
// instantiate func slicestringcopy(to any, fr any) int
fn := syslook("slicestringcopy")
fn = substArgTypes(fn, l1.Type, l2.Type)
ncopy = mkcall1(fn, types.Types[TINT], &nodes, nptr1, nptr2)
} else {
// instantiate func slicecopy(to any, fr any, wid uintptr) int
fn := syslook("slicecopy")
fn = substArgTypes(fn, l1.Type, l2.Type)
ncopy = mkcall1(fn, types.Types[TINT], &nodes, nptr1, nptr2, nodintconst(elemtype.Width))
}
} else {
// memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T))
nptr1 := nod(OINDEX, s, nod(OLEN, l1, nil))
nptr1.SetBounded(true)
nptr1 = nod(OADDR, nptr1, nil)
nptr2 := nod(OSPTR, l2, nil)
nwid := cheapexpr(conv(nod(OLEN, l2, nil), types.Types[TUINTPTR]), &nodes)
nwid = nod(OMUL, nwid, nodintconst(elemtype.Width))
// instantiate func memmove(to *any, frm *any, length uintptr)
fn := syslook("memmove")
fn = substArgTypes(fn, elemtype, elemtype)
ncopy = mkcall1(fn, nil, &nodes, nptr1, nptr2, nwid)
}
ln := append(nodes.Slice(), ncopy)
typecheckslice(ln, ctxStmt)
walkstmtlist(ln)
init.Append(ln...)
return s
}
代碼相對複雜,但func的註釋給我們提供了極好的僞代碼來說明其具體過程,func實際就是僞代碼的具體實施。此處將兩者結合下來看下大致處理過程:
- 計算追加後slice的總長度n
- 如果總長度n大於原cap,則調用growslice func進行擴容(cap最小爲n,具體擴容規則見growslice)
- 對擴容後的slice進行切片,長度爲n,獲取slice s,用以存儲所有的數據
- 根據不同的數據類型,調用對應的複製方法,將原slice及追加的slice的數據複製到新的slice
extendslice、walkappend也存在調用growslice的過程,現在一起來了解growslice的詳細過程吧。
growslice
growslice是在append的過程中原slice的剩餘空間不足以容納追加的元素時調用的。調用時,指定的cap爲追加元素後slice的總長度。
注意:func指定的cap並不一定是擴容後slice的最終cap,具體原因看源碼。
// 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 {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
...
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
}
memmove(p, old.array, lenmem)//拷貝數據至新分配的數組中
return slice{p, old.len, newcap}
}
擴容邏輯:
- 原cap擴容一倍,即doublecap
- 如果指定cap大於doublecap則使用cap,否則執行如下
- 如果原數據長度小於1024,則使用doublecap
- 否則在原cap的基礎上每次擴容1/4,直至不小於cap
擴容整體處理:
- 安裝原slice的cap及指定cap計算擴容後的cap
- 根據計算出cap申請內存(創建新的數組)
- 將原slice的數據拷貝到新內存中(新數組)
- 返回新slice,新slilce指向新數組,len爲原slice的len,cap爲擴容後的cap
正常我們使用,因slice的長度相對較小,append是擴容使用的是doublecap。
與reflect的Append比較,兩者主要的區別在於,growslice是指定容量的擴容,Append是基於當前slice的數據進行擴容,兩者的具體處理基本一致,某種意義上可以說Append是growslice的一個具體的調用。
使用append後會產生新的slice,必須重新賦值到原slice上,才能更新原slice的數據。
Slice append的數據變動問題
結合以上append的具體處理過程,請回答以下代碼運行後,兩次append的data和list內的數據是什麼?
data := [10]int{}
slice := data[5:8]
slice = append(slice,9)// slice=? data=?
slice = append(slice,10,11,12)// slice=? data=?
答案是:
//第一次append後結果
slice=[0 0 0 9]
data=[0 0 0 0 0 0 0 0 9 0]
//第二次append後結果
[0 0 0 9 10 11 12]
[0 0 0 0 0 0 0 0 9 0]
可以看到第一次append的結果影響到了原data的數據,第二次append的結果並沒有影響到了data的數據,這是爲什麼呢?
未append前,slice的cap是5。第一次append一個元素,未超出cap,因此直接存入數據到數組中。第二次append三個元素,append後的元素長度爲7,已大於原slice的cap,因此slice需要擴容,擴容後創建了新的數組,複製了data的數據到新數組內,然後存入append的數據,變動的是新數組,原數組data自然不受影響。
append存在對原數據影響的情況,使用時還是需要注意,如有必要,先copy原數據後再進行slice的操作。
總結
本文從反射及非反射兩種角度的源碼來探尋append的具體處理過程,對比後,可以發現兩者的處理邏輯一致。這給我們一些思路:如發現正面的調用我們無法理解時,可以試試找到其反射對應的處理看看是否更好理解些。
最後,將本文探討的主要內容總結如下:
-
slice本身並非指針,append追加元素後,意味着底層數組數據(或數組)、len、cap會發生變化,因此append後需要返回新的slice。
-
append在追加元素時,當前cap足夠容納元素,則直接存入數據,否則需要擴容後重新創建新的底層數組,拷貝原數組元素後,再存入追加元素。
-
cap的擴容意味着內存的重新分配,數據的拷貝等操作,爲了提高append的效率,若是能預估cap的大小的話,儘量提前聲明cap,避免後期的擴容操作。
公衆號
鄙人剛剛開通了公衆號,專注於分享Go開發相關內容,望大家感興趣的支持一下,在此特別感謝。