深入瞭解Go Slice(三)—— append的處理過程

前言

深入瞭解Go Slice(一)—— make的詳細處理過程

深入瞭解Go Slice(二)—— 切片的詳細處理過程

這兩篇文章分別介紹了從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處理過程如下:

  1. 判斷當前slice長度i0與追加數據的總長度i1是否溢出,溢出則報錯;
  2. 若i1小於/等於slice的cap(底層數組的長度),直接返回原slice的起始及結束數據部分
  3. 否則,當前底層數組已無法存儲所有的追加數據,需要進行擴容處理:
  • 若當前cap爲0,則直接已追加數據的長度爲新cap;

  • 若i1大於slice的cap m,開始逐步擴容cap,直至大於總數據總長i1

    • 若原數據長度i0<1024,則m翻倍;
    • 否則,m自增1/4
  • 構建新的Slice

  • 將原slice的數據拷貝至新slice中,並返回新slice。

  1. 將追加的數據存入指定的位置中

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種情況:

  1. 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實際就是僞代碼的具體實施。此處將兩者結合下來看下大致處理過程:

  1. 計算追加後slice的總長度n
  2. 如果總長度n大於原cap,則調用growslice func進行擴容(cap最小爲n,具體擴容規則見growslice)
  3. 對擴容後的slice進行切片,長度爲n,獲取slice s,用以存儲所有的數據
  4. 根據不同的數據類型,調用對應的複製方法,將原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

擴容整體處理:

  1. 安裝原slice的cap及指定cap計算擴容後的cap
  2. 根據計算出cap申請內存(創建新的數組)
  3. 將原slice的數據拷貝到新內存中(新數組)
  4. 返回新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開發相關內容,望大家感興趣的支持一下,在此特別感謝。

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