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時的遷移
相關文章
相關標籤/搜索