Go切片(Slice)浅析

673 阅读3分钟

前言

由于 Go的 Array创建之后长度就固定了,因此我们会更常用到 Slice。 在本篇里我们将从Slice的创建,访问,增加,复制几个方面来简单了解下Slice的原理。

struct

下面是Slice在runtime的结构体

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

创建

在编译时的Phase 6阶段,会进行逃逸分析,如果Slice发生逃逸或非常大时,会调用makeSlice64在堆上创建,否则就会在栈上或者静态存储区创建Slice。

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是一片连续的地址,访问就是从头往后数,具体逻辑在编译时的typecheck1的OINDEX中。


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中,会经过一系列转化之后,调用到runtime的growslice方法,growslice会通过容量的大小进行不同的扩容策略,最后在进行迁移。


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对内存进行复制。

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时的迁移