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会额外包含数组的界(元素个数)

//代码地址

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也是底层数组的长度。

//代码地址

// 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的判断。

// 代码地址

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时会根据情况使用不同的方式初始化切片。当数组逃逸到堆上或者切片容量非常大时会在堆上初始化,而不满足时会在栈上初始化。这个区别也可以在生成的汇编中发现指令片段

// 代码地址

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是在堆上申请对应大小的内存,在这里不再展开。

// 代码地址

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。

// 代码地址

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
复制代码

访问

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

func (s *state) expr(n *Node) *ssa.Value {

    ...

    switch n.Op{

    ...

    case OLEN, OCAP: // 代码地址

                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: // 代码地址

                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补充len与cap 得到新的切片。

如果赋值回原切片,与上述过程类似,区别在于扩容后会更新底层数组与容量,最后会更新长度而不需要调用makeslice。

// 代码地址



// 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,newcap为old.cap*2
    2. 如果old.cap大于等于1024,newcap为old.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)

    }
复制代码

计算完lenmem与capmem后会创建一段连续的大小为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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享