阅读 479

Golang Slice 学习解析

map解析: juejin.cn/post/695470…

介绍

基本概念

Golang中的Slice(切片)是动态数组,支持追加、随机访问、遍历、拷贝等操作。Slice在元素数量增长时可以自动扩容,也允许对通过对已有切片进行截取获得一个更小的切片。

基本操作

// 1.初始化 []T
a := []int{0, 1, 2, 3} // 1.1 字面量初始化
a := []int{2: 2, 3, 0: 0, 1}
a := make([]int, 4)  // 1.2 make初始化,参数为len
a := make([]int, 0, 4) // 参数为(len, cap)  
a = b[1:2] // 1.3切割已有切片(/数组)初始化 参数为(lower_bound, upper_bound)
a = b[1:2:2] // (lower_bound, upper_bound, cap_bound)
// 2.访问
sum := a[0] + a[2]
l, c := len(a), cap(a)
// 3.遍历
for range a {}
for index := range a {} // index
for index, value := range a {}  // (index, value)  
for i := 0; i < len(a); i ++ {a[i]=0}
// 4.追加
a = append(a, 4)
// 5:拷贝
copy(dst, src) 
复制代码

与数组的关联

Slice对元素的操作基于数组。在「内存模型」部分将会看到,切片的元素是存储在一块连续的内存中,这块内存便是slice的底层数组。

源码解析

源码基于go1.16源码下载

内存模型

编译期间 在编译期间,slice的类型仅包含元素的类型字段。而array会额外包含数组的界(元素个数)

//[代码地址](https://github.com/golang/go/blob/go1.16/src/cmd/compile/internal/types/type.go#L345)
type Array struct {
        Elem  *Type // element type
        Bound int64 // number of elements; <0 if unknown yet
}

type Slice struct {
        Elem *Type // element type
}
复制代码

运行时 slice类型为reflect.SliceHeader,包含指向底层数组的指针Data,切片长度Len与切片容量Cap。实际上Cap也是底层数组的长度。

//[代码地址](https://github.com/golang/go/blob/go1.16/src/reflect/value.go#L1994)
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.(这里的data并不能保证底层数组不被gc,所以程序必须维护另一份底层数据的正确类型指针)
type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}
复制代码
e.g.
a := make([]int, 3, 5)
a[0], a[1], a[2] = 0, 1, 2
复制代码

初始化

Slice的初始化可以分为三类:字面量初始化、关键字初始化(make)与截取初始化。 字面量 字面量初始化即用类似 []T {...} 的语句得到一个初始化的切片。我们使用 a := []int{1, 2, 3, 4}b := []int{2: 3, 4, 0: 1, 2} 来生成对应的汇编指令观察发生了什么。 根据指令片段,可以发现字面量初始化时发生了如下的操作:

  1. 创建一个长度为4的int数组并为其分配内存
LEAQ        type.[4]int(SB), AX
CALL        runtime.newobject(SB)
复制代码
  1. 将元素值依次放入数组中
  2. 将数组、长度(4)与容量(4)赋值给变量的对应字段
MOVQ        AX, "".a+120(SP)
MOVQ        $4, "".a+128(SP)
MOVQ        $4, "".a+136(SP)
复制代码

b的初始化也是同样的,只不过元素赋值的顺序有改变,按照下标为2-3-0-1的顺序赋值。未指定下标的元素使用的下标,是前一个元素的下标+1(如果没有为0),而当元素下标有冲突时会在编译时报错。

// 大概是如何确定字面量初始化时的下标...
func (index *int, lastIndex *int) int {
    if index != nil {
        return *index
    } else if lastIndex != nil {
        return *lastIndex + 1
    } else {
        return 0
    }
}
复制代码

关键字 不同于字面量初始化中很多工作都在编译时完成,make初始化很多内容都需要运行时去完成。 make需要指定切片的长度,可选指定切片的容量,入参的校验工作在类型检查期间完成。类型检查包括对len和cap的类型、数值检查(常数时)与cap是否大于等于len的判断。

// [代码地址](https://github.com/golang/go/blob/go1.16/src/cmd/compile/internal/gc/typecheck.go#L1751)
func typecheck1(n *Node, top int) (res *Node) {
        ...
        switch n.Op {
        ...
        case OMAKE:
                args := n.List.Slice()

                i := 1
                switch t.Etype {
                case TSLICE:
                        if i >= len(args) { // 检查参数len是否存在
                                yyerror("missing len argument to make(%v)", t)
                                n.Type = nil
                                return n
                        }

                        l = args[i]
                        i++
                        l = typecheck(l, ctxExpr) // 检查len参数对应节点的类型
                        var r *Node
                        if i < len(args) { // 如果参数超过2个则是cap参数
                                r = args[i]
                                i++
                                r = typecheck(r, ctxExpr) // 检查cap参数对应节点的类型
                        }

                        if l.Type == nil || (r != nil && r.Type == nil) {
                                n.Type = nil
                                return n
                        }
                        // 检查len与cap是常数的情况下,len与cap的值是否在允许的范围内
                        if !checkmake(t, "len", &l) || r != nil && !checkmake(t, "cap", &r) { 
                                n.Type = nil
                                return n
                        }
                        // 检查len与cap均为常数的情况下,cap是否满足>=len的限制
                        if Isconst(l, CTINT) && r != nil && Isconst(r, CTINT) && l.Val().U.(*Mpint).Cmp(r.Val().U.(*Mpint)) > 0 {
                                yyerror("len larger than cap in make(%v)", t)
                                n.Type = nil
                                return n
                        }

                        n.Left = l
                        n.Right = r
                        n.Op = OMAKESLICE
                }
        ...
        }
}
复制代码

在校验工作结束后,会将节点的值替换为OMAKESLICE。而在OMAKESLICE时会根据情况使用不同的方式初始化切片。当数组逃逸到堆上或者切片容量非常大时会在堆上初始化,而不满足时会在栈上初始化。这个区别也可以在生成的汇编中发现指令片段

// [代码地址](https://github.com/golang/go/blob/go1.16/src/cmd/compile/internal/gc/walk.go#L1316)
func walkexpr(n *Node, init *Nodes) *Node {
    ...
    siwtch n.Op {
    ...
    case OMAKESLICE:
        l := n.Left
        r := n.Right
        if r == nil {
                r = safeexpr(l, init)
                l = r
        }
        ...
        if n.Esc == EscNone { // 未发生逃逸
            t = types.NewArray(t.Elem(), i) // 创建一个[r]T的数组var_
            var_ := temp(t)
            a := nod(OAS, var_, nil) // zero temp
            a = typecheck(a, ctxStmt)
            init.Append(a)
            r := nod(OSLICE, var_, nil)  
            r.SetSliceBounds(nil, l, nil) // 截取var_的[:l]
            r = conv(r, n.Type)
            r = typecheck(r, ctxExpr)
            r = walkexpr(r, init)
            n = r
        } else {
            // n escapes; set up a call to makeslice.
            // When len and cap can fit into int, use makeslice instead of
            // makeslice64, which is faster and shorter on 32 bit platforms.
            // 发生了逃逸需要调用makeslice初始化,根据len和cap是否在int范围内决定使用makeslice还是makeslice64

            len, cap := l, r

            fnname := "makeslice64"
            argtype := types.Types[TINT64]

            // Type checking guarantees that TIDEAL len/cap are positive and fit in an int.
            // The case of len or cap overflow when converting TUINT or TUINTPTR to TINT
            // will be handled by the negative range checks in makeslice during runtime.
            // 接下来的类型检查保证字面量类型的是正数并且可以转换为int范围内;
            // len和cap在转换为uint、uintptr、int溢出的情况在makeslice的范围检查中进行
            if (len.Type.IsKind(TIDEAL) || maxintval[len.Type.Etype].Cmp(maxintval[TUINT]) <= 0) &&
                    (cap.Type.IsKind(TIDEAL) || maxintval[cap.Type.Etype].Cmp(maxintval[TUINT]) <= 0) {
                    fnname = "makeslice"
                    argtype = types.Types[TINT]
            }

            m := nod(OSLICEHEADER, nil, nil) // 这里构建的是sliceHeader节点
            m.Type = t

            fn := syslook(fnname)
            // 接下来进行makeslice的调用
            m.Left = mkcall1(fn, types.Types[TUNSAFEPTR], init, typename(t.Elem()), conv(len, argtype), conv(cap, argtype))
            m.Left.MarkNonNil()
            m.List.Set2(conv(len, types.Types[TINT]), conv(cap, types.Types[TINT])) // 节点list中增加len与cap

            m = typecheck(m, ctxExpr) // typecheck接下来会调用typecheck1来构建sliceHeader的其他信息
            m = walkexpr(m, init)
            n = m
        }   
    }
}
复制代码

栈上的初始化是先在栈上初始化一个长度为cap的T类型的数组,而后进行[:len]的截取获取对应的切片。

tmp := [cap]T{}
ret := tmp[:len]
复制代码

堆上的初始化,会调用runtime.makeslice,成功时会在堆上申请一块连续的内存。首先会进行一些参数检查,包括使用的内存空间是否溢出、申请的内存大小是否超过限制、len是否是负数或len是否超过cap。之后的mallocgc是在堆上申请对应大小的内存,在这里不再展开。

// [代码地址](https://github.com/golang/go/blob/go1.16/src/runtime/slice.go#L83)
func makeslice(et *_type, len, cap int) unsafe.Pointer {
        mem, overflow := math.MulUintptr(et.size, uintptr(cap))
        if overflow || mem > maxAlloc || len < 0 || len > cap {
                // NOTE: Produce a 'len out of range' error instead of a
                // 'cap out of range' error when someone does make([]T, bignumber).
                // 'cap out of range' is true too, but since the cap is only being
                // supplied implicitly, saying len is clearer.
                // See golang.org/issue/4085.
                // 实际上是容量超限,但大部分情况下cap是隐式指定的,所以说是长度超限。
                mem, overflow := math.MulUintptr(et.size, uintptr(len))
                if overflow || mem > maxAlloc || len < 0 {
                        panicmakeslicelen()
                }
                panicmakeslicecap()
        }

        return mallocgc(mem, et, true)
}
复制代码

在makeslice调用完成后,会进入OSliceHeader的typecheck,进一步填充sliceHeader的len与cap。

// [代码地址](https://github.com/golang/go/blob/go1.16/src/cmd/compile/internal/gc/typecheck.go#L1120)
func typecheck1(n *Node, top int) (res *Node) {
        ...
        switch n.Op {
        ...
        case OSLICEHEADER:
                // Errors here are Fatalf instead of yyerror because only the compiler
                // can construct an OSLICEHEADER node.
                // Components used in OSLICEHEADER that are supplied by parsed source code
                // have already been typechecked in e.g. OMAKESLICE earlier.
                // 因为只有编译器能构建OSliceHeader节点所以这里的error是fatal
                // 构成OSliceHeader的元素之前的节点(如OMakeSlice)检查过了
                ok |= ctxExpr

                t := n.Type
                if t == nil { //检查是否有类型
                        Fatalf("no type specified for OSLICEHEADER")
                }

                if !t.IsSlice() { // 检查是否是slice类型
                        Fatalf("invalid type %v for OSLICEHEADER", n.Type)
                }

                if n.Left == nil || n.Left.Type == nil || !n.Left.Type.IsUnsafePtr() { // 检查data是否已被赋值
                        Fatalf("need unsafe.Pointer for OSLICEHEADER")
                }

                if x := n.List.Len(); x != 2 { // 检查节点list长度
                        Fatalf("expected 2 params (len, cap) for OSLICEHEADER, got %d", x)
                }

                n.Left = typecheck(n.Left, ctxExpr)
                l := typecheck(n.List.First(), ctxExpr)
                c := typecheck(n.List.Second(), ctxExpr)
                l = defaultlit(l, types.Types[TINT])
                c = defaultlit(c, types.Types[TINT])

                if Isconst(l, CTINT) && l.Int64Val() < 0 { // 检查len是否非负
                        Fatalf("len for OSLICEHEADER must be non-negative")
                }

                if Isconst(c, CTINT) && c.Int64Val() < 0 { // 检查cap是否非负
                        Fatalf("cap for OSLICEHEADER must be non-negative")
                }
                // 检查len <= cap
                if Isconst(l, CTINT) && Isconst(c, CTINT) && l.Val().U.(*Mpint).Cmp(c.Val().U.(*Mpint)) > 0 {
                        Fatalf("len larger than cap for OSLICEHEADER")
                }

                n.List.SetFirst(l)
                n.List.SetSecond(c)
        ...
        }
}
复制代码

截取 截取初始化类似关键字初始化中栈上的初始化,会复用目标源切片的底层数组,重新对len与cap赋值。

{
    a := []int{1,2,3,4}
    b := a[1:2]
}

0x0057 00087 (main.go:4)        MOVQ        AX, "".a+64(SP) // data
0x005c 00092 (main.go:4)        MOVQ        $4, "".a+72(SP) // len
0x0065 00101 (main.go:4)        MOVQ        $4, "".a+80(SP) // cap
0x006e 00110 (main.go:5)        JMP        112
0x0070 00112 (main.go:5)        JMP        114
0x0072 00114 (main.go:5)        ADDQ        $8, AX          // t=a[1:]
0x0076 00118 (main.go:5)        MOVQ        AX, "".b+40(SP) // data=t
0x007b 00123 (main.go:5)        MOVQ        $1, "".b+48(SP) // len=1
0x0084 00132 (main.go:5)        MOVQ        $3, "".b+56(SP) // cap=3
复制代码

访问

访问切片的lencap 即读取SliceHeader的对应字段。切片底层是数组支持随机访问,通过下标访问也是读取data中的对应值。遍历中的访问将在之后的「遍历」中介绍。

func (s *state) expr(n *Node) *ssa.Value {
    ...
    switch n.Op{
    ...
    case OLEN, OCAP: // [代码地址](https://github.com/golang/go/blob/go1.16/src/cmd/compile/internal/gc/ssa.go#L2725)
                switch {
                ...
                case n.Left.Type.IsSlice():
                        op := ssa.OpSliceLen
                        if n.Op == OCAP {
                                op = ssa.OpSliceCap
                        }
                        return s.newValue1(op, types.Types[TINT], s.expr(n.Left)) // 解读为int类型
                ...
                }
    ...
    case OINDEX: // [代码地址](https://github.com/golang/go/blob/go1.16/src/cmd/compile/internal/gc/ssa.go#L2676)
                switch {
                .​..
                case n.Left.Type.IsSlice():
                        p := s.addr(n) // 取地址
                        return s.load(n.Left.Type.Elem(), p) // load为对应类型
                ...
                }
    ...
    }
}
复制代码

遍历

遍历分为经典循环和golang较特有的range循环 经典循环 经典循环是包含四块,分别是

  1. 初始化循环的Ninit
  2. 循环的继续条件Left
  3. 循环体结束时执行的Right
  4. 循环体NBody
for Ninit; Left; Right {
    NBody
}
复制代码

这个各语言的实现基本是类似的,就不再赘述了。 range循环 range其实是golang提供的语法糖,range一个数组/切片,在编译时会将其转换为经典循环的形式。

ha := a
hv1 := 0
hn := len(ha) //切片长度
v1 := hv1     //index
v2 := nil     //value
for ; hv1 < hn; hv1++ {
    tmp := ha[hv1]
    v1, v2 = hv1, tmp
    ...
}
复制代码

追加

在源码的注释中描述的比较清晰,流程大体上由是否赋值回原切片(inplace)区分。 如果不赋值回原切片,会首先判断追加后的长度是否超过原切片的容量,如果超过会调用growslice扩容,之后将元素追加到底层数组的末尾,最后调用makeslice补充lencap 得到新的切片。 如果赋值回原切片,与上述过程类似,区别在于扩容后会更新底层数组与容量,最后会更新长度而不需要调用makeslice

// [代码地址](https://github.com/golang/go/blob/go1.16/src/cmd/compile/internal/gc/ssa.go#L2841)

// If inplace is false, process as expression "append(s, e1, e2, e3)":

ptr, len, cap := s
newlen := len + 3
if newlen > cap {
    ptr, len, cap = growslice(s, newlen)
    newlen = len + 3 // recalculate to avoid a spill
}
// with write barriers, if needed:
*(ptr+len) = e1
*(ptr+len+1) = e2
*(ptr+len+2) = e3
return makeslice(ptr, newlen, cap)


// If inplace is true, process as statement "s = append(s, e1, e2, e3)":

a := &s
ptr, len, cap := s
newlen := len + 3
if uint(newlen) > uint(cap) {
   newptr, len, newcap = growslice(ptr, len, cap, newlen)
   vardef(a)       // if necessary, advise liveness we are writing a new a
   *a.cap = newcap // write before ptr to avoid a spill
   *a.ptr = newptr // with write barrier
}
newlen = len + 3 // recalculate to avoid a spill
*a.len = newlen
// with write barriers, if needed:
*(ptr+len) = e1
*(ptr+len+1) = e2
*(ptr+len+2) = e3
复制代码

扩容

代码地址 扩容即追加中的growslice。首先是确定新切片的容量,目前新容量的计算方式如下:

  1. 如果所需的容量cap大于原切片容量old.cap的两倍,新容量newcap更新为cap
  2. 如果cap小于等于old.cap*2
    1. 如果old.cap小于1024,newcapold.cap*2
    2. 如果old.cap大于等于1024,newcapold.cap*1.25
func growslice(et *_type, old slice, cap int) slice {
    ...
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
            newcap = cap
    } else {
            if old.cap < 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
                    }
            }
    }
    ...
}
复制代码

接下来会计算新切片所需的内存大小,默认情况下会直接通过cap * et.size 获取需要的内存大小,但是对一些特殊的元素大小(1, 指针大小, 2的整数次幂)会进行计算的优化。最后都会通过roundupsize对计算出的内存进行对齐操作以减少碎片,并重新更新newcap

    // Specialize for common values of et.size.
    // For 1 we don't need any division/multiplication.
    // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
    // For powers of 2, use a variable shift.
    // 对于1不需要进行乘法;对于指针大小可以乘以常量; 对于2的整数幂可以左移计算
    switch {
    case et.size == 1:
            lenmem = uintptr(old.len) // 不需要乘法
            newlenmem = uintptr(cap)
            capmem = roundupsize(uintptr(newcap)) // 内存向上取整
            overflow = uintptr(newcap) > maxAlloc
            newcap = int(capmem)
    case et.size == sys.PtrSize:
            lenmem = uintptr(old.len) * sys.PtrSize // 乘一个常量
            newlenmem = uintptr(cap) * sys.PtrSize
            capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
            overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
            newcap = int(capmem / sys.PtrSize) // 重新计算newcap
    case isPowerOfTwo(et.size):
            var shift uintptr // 左移的位数
            if sys.PtrSize == 8 { // 掩码
                    // Mask shift for better code generation.
                    shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 
            } else {
                    shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
            }
            lenmem = uintptr(old.len) << shift
            newlenmem = uintptr(cap) << shift
            capmem = roundupsize(uintptr(newcap) << shift)
            overflow = uintptr(newcap) > (maxAlloc >> shift)
            newcap = int(capmem >> shift)
    default:
            lenmem = uintptr(old.len) * et.size // 普通乘法
            newlenmem = uintptr(cap) * et.size
            capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
            capmem = roundupsize(capmem)
            newcap = int(capmem / et.size)
    }
复制代码

计算完lenmemcapmem后会创建一段连续的大小为capmem的内存并清空不会被覆写区域[lenmem, capmem)的内存,最后通过memmove将原切片的数据拷贝到新的数组中。

        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.
                        // 实际上在malloc时已经清空了,这里是对新的数组染色
                        bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
                }
        }
        memmove(p, old.array, lenmem) // 拷贝数据

        return slice{p, old.len, newcap}
复制代码

拷贝

golang 内置了copy函数,可以用来copy slice(也可以把string copy到slice),函数的声明在src/builtin/builtin.go中,copy会有防溢出设置,返回值是拷贝的元素数,返回值会等于src和dst长度的较小值( return value = min(len(src),len(dst)))。

// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int
复制代码

builtin.go中只有函数的声明,具体函数的实现还得看汇编代码,用go tool compile -S即可查看具体的汇编实现,来看一段程序。 go源代码:

package main

func main() {

}

func myCopy(src []int64) {
   dst := make([]int64, 0)
   copy(dst, src)
   _ = dst
}
复制代码

关键汇编代码:


0x0037 00055 (main.go:9)   MOVQ   "".dst+128(SP), AX //dst.data
0x003f 00063 (main.go:9)   MOVQ   "".dst+136(SP), CX //dst.len
0x0047 00071 (main.go:9)   MOVQ   "".dst+144(SP), DX //dst.cap
0x004f 00079 (main.go:9)   MOVQ   AX, ""..autotmp_3+88(SP)  //dst.data
0x0054 00084 (main.go:9)   MOVQ   CX, ""..autotmp_3+96(SP)  //dst.len
0x0059 00089 (main.go:9)   MOVQ   DX, ""..autotmp_3+104(SP) //dst.cap
0x005e 00094 (main.go:9)   MOVQ   "".dst+40(SP), AX //src.data
0x0063 00099 (main.go:9)   MOVQ   "".dst+48(SP), CX //src.len
0x0068 00104 (main.go:9)   MOVQ   "".dst+56(SP), DX //src.cap
0x006d 00109 (main.go:9)   MOVQ   AX, ""..autotmp_4+64(SP)  //src.data
0x0072 00114 (main.go:9)   MOVQ   CX, ""..autotmp_4+72(SP)  //src.len
0x0077 00119 (main.go:9)   MOVQ   DX, ""..autotmp_4+80(SP)  //src.cap
0x007c 00124 (main.go:9)   MOVQ   ""..autotmp_3+96(SP), AX
0x0081 00129 (main.go:9)   MOVQ   AX, ""..autotmp_5+32(SP) //sp+32 = dst.len
0x0086 00134 (main.go:9)   CMPQ   ""..autotmp_4+72(SP), AX //src.len   dst.len
0x008b 00139 (main.go:9)   JLT    143  //如果src.len < dst.len 则跳到143
0x008d 00141 (main.go:9)   JMP 227  //否则,跳到227(调到155)

0x008f 00143 (main.go:9)   MOVQ   ""..autotmp_4+72(SP), AX
0x0094 00148 (main.go:9)   MOVQ   AX, ""..autotmp_5+32(SP) //sp+32 = src.len (这几行代码的作用是取src.len和dst.len的较小值,以确定copy的字节数)  (src+32 = min(src.len,dst.len))
0x0099 00153 (main.go:9)   JMP 155

0x009b 00155 (main.go:9)   MOVQ   ""..autotmp_4+64(SP), AX //src.data放入AX
0x00a0 00160 (main.go:9)   CMPQ   ""..autotmp_3+88(SP), AX //比较dst.data和src.data
0x00a5 00165 (main.go:9)   JNE    169 //如果src.data!=dst.data 跳到169
0x00a7 00167 (main.go:9)   JMP 225 //否则,跳到225(src和dst的data相等,无须copy)

0x00a9 00169 (main.go:9)   MOVQ   ""..autotmp_5+32(SP), AX //min(src.len,dst.len) 放入AX
0x00ae 00174 (main.go:9)   MOVQ   AX, ""..autotmp_6+24(SP) //sp+24 = minlen
0x00b3 00179 (main.go:9)   MOVQ   ""..autotmp_3+88(SP), CX //dst.data放入cx
0x00b8 00184 (main.go:9)   MOVQ   ""..autotmp_4+64(SP), DX //src.data放入dx

0x00bd 00189 (main.go:9)   MOVQ   CX, (SP) //dst.data放入(sp)(第一个参数)
0x00c1 00193 (main.go:9)   MOVQ   DX, 8(SP) //src.data放入(sp+8)(第二个参数)
0x00c6 00198 (main.go:9)   SHLQ   $3, AX //minlen 左移三位(因为我们的切片是int类型,int类型的长度为8Byte,所以总的copy的字节数就是minlen*8,也就等同于minlen << 3)
0x00ca 00202 (main.go:9)   MOVQ   AX, 16(SP) //放入(sp+16)(minlen << 3)(第三个参数)
0x00cf 00207 (main.go:9)   PCDATA $1, $1
0x00cf 00207 (main.go:9)   CALL runtime.memmove(SB) //memmove的三个参数准备完毕,调用runtime.memmove
0x00d4 00212 (main.go:9)   JMP 214 //退出函数


0x00d6 00214 (main.go:11)  PCDATA $1, $-1
0x00d6 00214 (main.go:11)  MOVQ   112(SP), BP
0x00db 00219 (main.go:11)  ADDQ   $120, SP
0x00df 00223 (main.go:11)  NOP
0x00e0 00224 (main.go:11)  RET

0x00e1 00225 (main.go:9)   JMP 214
0x00e3 00227 (main.go:9)   JMP 155
0x00e5 00229 (main.go:9)   NOP
复制代码

可以看到,copy并没有像map的make那样,直接有runtime的实现,copy的实现是汇编代码+runtime.memmove。下面让我们来看下runtime.memmove,函数的原型很简单,至于函数的实现为了性能上的优化,golang采用了汇编代码实现,与cpu架构有关(有一篇详解的文章:mzh/blog)。

// memmove copies n bytes from "from" to "to".
//
// memmove ensures that any pointer in "from" is written to "to" with
// an indivisible write, so that racy reads cannot observe a
// half-written pointer. This is necessary to prevent the garbage
// collector from observing invalid pointers, and differs from memmove
// in unmanaged languages. However, memmove is only required to do
// this if "from" and "to" may contain pointers, which can only be the
// case if "from", "to", and "n" are all be word-aligned.
//
// Implementations are in memmove_*.s.
//
//go:noescape
func memmove(to, from unsafe.Pointer, n uintptr)
复制代码

runtime/slice.go中还可以看到有slicecopy这个函数,这个函数会被反射包的切片拷贝reflect.Copy使用。

func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
   if fromLen == 0 || toLen == 0 {
      return 0
   }

   n := fromLen
   if toLen < n {
      n = toLen
   }

   if width == 0 {
      return n
   }

   size := uintptr(n) * width
   //...

   if size == 1 { // common case worth about 2x to do here
      // TODO: is this still worth it with new memmove impl?
 *(*byte)(toPtr) = *(*byte)(fromPtr) // known to be a byte pointer
   } else {
      memmove(toPtr, fromPtr, size)
   }
   return n
}
复制代码

使用时的一些注意点

  1. slice使用底层数组,在slice不释放时,底层数组也无法释放,这会导致,如果对大量的bigger slice做截取操作,可能会导致隐含的内存泄露,正确的做法是使用copy将小内存copy出来。

func main() {
   s2 := Mem2() //385024
   printMem()
   s2 = nil
   
   s1 := Mem1() //800415744
   printMem()
   _, _ = s1, s2
   fmt.Println(s2)
   fmt.Println(s1)
}

func Mem1() []int64 {
   s := make([]int64, 100_000_000)
   return s[:10]
}

func Mem2() []int64 {
   s := make([]int64, 100_000_000)
   dst := make([]int64, 10)
   copy(dst, s)
   return dst
}

func printMem() {
   runtime.GC()
   s := runtime.MemStats{}
   runtime.ReadMemStats(&s)
   fmt.Println(s.HeapInuse)
}
//425984
//800448512
复制代码
  1. range一个slice时,不要直接对index和value取地址,因为index和value并不是在每次遍历都会生成临时变量。
func Print(dst []*int64) {
   for _, v := range dst {
      fmt.Println(*v)
   }
}

func Range1() {
   src := []int64{1, 2, 3}
   dst := make([]*int64, 3)
   for i, v := range src {
      dst[i] = &v
   }
   Print(dst)
}
//输出的结果是3,3,3
复制代码

正确的做法是对src[i]取地址。

func Range2() {
   src := []int64{1, 2, 3}
   dst := make([]*int64, 3)
   for i := range src {
      dst[i] = &src[i]
   }
   Print(dst)
}
复制代码
  1. 在多个切片共用同一个底层数组时,注意改动数据带来的影响
func main() {
    var (
        secret = []byte("secret")
        data   = []byte("data_raw")

        flag = false
    )

    genMD5 := func(d []byte) []byte {
        s := md5.New()
        s.Write(append(d, secret...))
        return s.Sum(nil)
    }

    encode := func(d []byte) []byte {
        return append(d, genMD5(d)...)
    }

    getMD5 := func(d []byte) ([]byte, []byte) {
        if !flag {
            return d[:len(d)-16], d[len(d)-16:]
        } else {
            return d[: len(d)-16 : len(d)-16], d[len(d)-16:]
        }
    }

    decode := func(d []byte) ([]byte, error) {
        data, digest := getMD5(d)
        if calcDigest := genMD5(data); bytes.Compare(calcDigest, digest) != 0 {
            return nil, fmt.Errorf("check error, calc_digest=%x, attached_digest=%x", string(calcDigest), string(digest))
        }
        return data, nil
    }

    eData := encode(data)
    dData, err := decode(eData)
    fmt.Printf("data=%+v, err=%+v\n", string(dData), err)

    flag = true
    eData = encode(data)
    dData, err = decode(eData)
    fmt.Printf("data=%+v, err=%+v\n", string(data), err)
    return
}
/*
data=, err=check error, calc_digest=a88000e689a266fc62a0328fd80a5aec, attached_digest=73656372657466fc62a0328fd80a5aec
data=data_raw, err=<nil>
*/
复制代码

参考资料

Go 语言切片的实现原理 | Go 语言设计与实现 (draveness.me)
Go Slices: usage and internals - The Go Blog (golang.org)
如何确定一个 Go 变量会被分配在哪里? | RussellLuo

文章分类
后端
文章标签