Go切片(Slice)浅析

前言

因为 GoArray建立以后长度就固定了,所以咱们会更经常使用到 Slice。 在本篇里咱们将从Slice的建立,访问,增长,复制几个方面来简单了解下Slice的原理。golang

struct

下面是Sliceruntime的结构体数组

type slice struct {
	array unsafe.Pointer   // 指向切片头
	len   int   // 切片吧长度
	cap   int  // 切片容积
}

复制代码

建立

在编译时的Phase 6阶段,会进行逃逸分析,若是Slice发生逃逸或很是大时,会调用makeSlice64在堆上建立,不然就会在栈上或者静态存储区建立Sliceapp

walk

case OMAKESLICE:
  ...
  if n.Esc == EscNone {
    对于在栈上的将会转成如下相似代码
    // var arr [r]T
    // n = arr[:l]
    ...
  }else {
   对于在堆上建立的,将会调用makeslice64
   ...
    fnname := "makeslice64"
    ...
    m.Left = mkcall1(fn, types.Types[TUNSAFEPTR], init, typename(t.Elem()), conv(len, argtype), conv(cap, argtype))
    ...
  }

复制代码

makeslice

func makeslice(et *_type, len, cap int) unsafe.Pointer {
    校验cap长度
	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.
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}
    分配内存
	return mallocgc(mem, et, true)
}

复制代码

访问

Slice的访问在编译时就已经转化成了汇编代码,因为Slice的结构中的array指向的数组头的地址,并且Slice是一片连续的地址,访问就是从头日后数,具体逻辑在编译时的typecheck1OINDEX中。函数

func typecheck1(n *Node, top int) (res *Node) {
    ...
    switch n.Op {
	case OINDEX:
	...
	switch t.Etype {
	    case TSTRING, TARRAY, TSLICE:
			n.Right = indexlit(n.Right)
			if t.IsString() {
				n.Type = types.Bytetype
			} else {
				n.Type = t.Elem()
			}
			why := "string"
			if t.IsArray() {
				why = "array"
			} else if t.IsSlice() {
				why = "slice"
			}
			...
	}
}

复制代码

增长

Slice的插入,删除等操做其实都是经过append函数来进行的,在append中,会通过一系列转化以后,调用到runtimegrowslice方法,growslice会经过容量的大小进行不一样的扩容策略,最后在进行迁移。oop

func growslice(et *_type, old slice, cap int) slice {
    ...
    经过容量进行扩容选择,减小频繁扩容以后进行迁移
	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
			}
		}
	}
	...
	memmove(p, old.array, lenmem)

	return slice{p, old.len, newcap}
}

复制代码

复制

切片的复制是Slice在运行时里有实现,在编译时里,会经过HasHeapPointer来判断Slice是否存在堆上,是就会进行slicecopy操做,不是的话就会调用memmove对内存进行复制。ui

walk

func copyany(n *Node, init *Nodes, runtimecall bool) *Node {
    判断是否实在堆上建立
	if n.Left.Type.Elem().HasHeapPointer() {
		Curfn.Func.setWBPos(n.Pos)
		fn := writebarrierfn("typedslicecopy", n.Left.Type, n.Right.Type)
		return mkcall1(fn, n.Type, init, typename(n.Left.Type.Elem()), n.Left, n.Right)
	}
	....
	fn := syslook("memmove")
	fn = substArgTypes(fn, nl.Type.Elem(), nl.Type.Elem())
}
复制代码

slicecopy

func slicecopy(to, fm slice, width uintptr) int {
	if fm.len == 0 || to.len == 0 {
		return 0
	}

	n := fm.len
	if to.len < n {
		n = to.len
	}

	if width == 0 {
		return n
	}

	if raceenabled {
		callerpc := getcallerpc()
		pc := funcPC(slicecopy)
		racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
		racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
	}
	if msanenabled {
		msanwrite(to.array, uintptr(n*int(width)))
		msanread(fm.array, uintptr(n*int(width)))
	}
    经过一系列判断最后走下面的if进行复制
	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)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
	} else {
		memmove(to.array, fm.array, size)
	}
	return n
}
复制代码

结语

  • 减小Slice的逃逸,给GC减负
  • 合理分配cap,减小append时的迁移
相关文章
相关标签/搜索