golang源码分析-slice(切片)

159

golang的切片slice

GET知识点

  • 切片的扩容原理和设计
  • 切片容量的作用
  • 了解golang的内存对齐
  • 不指定容量,则默认长度和容量相等
  • 合理的指定容量,避免频繁的内存分配和数据copy。
  • 代码反编译,或者 下载dlv源代码调试工具可以起到学习辅助作用
go tool compile -S make.go # golang的代码的反编译
go install github.com/go-delve/delve/cmd/dlv@latest

参考链接

源代码

  • 代码基于 go1.17.8分析
  • 系统-Mac

数据结构

type slice struct {
	array unsafe.Pointer // 指向底层数组指针
	len   int // 长度
	cap   int // 容量
}
  • array 指向底层数组的指针
  • len,底层数组中元素的个数,即允许访问的个数
  • cap,容量,底层数组的size,当 len+要添加的元素个数 大于 cap是会发生扩容。

扩容机制(1)

func growslice(et *_type, old slice, cap int) slice {
	// 代码省略.....
  
	newcap := old.cap
	doublecap := newcap + newcap

  if cap > doublecap {  // cap:所需的最小容量 > old*2,则直接使用cap
		newcap = cap
	} else { 
		if old.cap < 1024 { // 2倍的分配需要有个限制,过多的分配可能会导致资源的浪费。所以这里以1024为分界线
			newcap = doublecap
		} else { // 合理配置一个所需的最小容量,已1024为分界线。
			for 0 < newcap && newcap < cap { // 不过多分配,
				newcap += newcap / 4 // 预估容量是扩容前容量的1.25倍,即以0.25的倍数增加
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
}	

cap > doublecap

0、未定义slice,cap大小,首次append的时候,比如:,var nums []int,

0、当addEle的元素大于 old*2的时候,比如: append(nums,添加元素个数的超过len(nums)的2倍多)

old.cap < 1024

0、以1024作为2倍的分配界限,避免分配的过多导致资源浪费,

0、如果超过1024则,每次以1.25倍的增量进行分配,直至分配出一个,minimum capacity,

扩容机制(2)

内存对齐,根据元素类型大小,重新计算所需的newcap数量,

这里的et.size指的是,元素类型大小,

roundupsize 向上取整,取最接近的大小

func growslice(et *_type, old slice, cap int) slice {
	// 代码省略.....

	switch {
	case et.size == sys.PtrSize: // 8
		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)
	}
}

// runtime/sizeclasses.go

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

扩容机制(3)

func growslice(et *_type, old slice, cap int) slice {
	// 代码省略.....
  
	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.
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
		}
	}
	memmove(p, old.array, lenmem)

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

0、分配内存,

0、将老数组内容copy到新分配好的数组中

0、返回新的slice

例子分析

1、了解slice扩容

func main() {
	nums := make([]int, 0)
	nums = append(nums, 1)
  
  // [1] 1 1

	fmt.Println(nums, len(nums), cap(nums))
}
  • 使用 dlv或者 go tool compile -S main.go(反编译)代码,只需要关注下面这个2个关键的
 runtime.makeslice(SB) // 初始化切片,并返回
 runtime.growslice(SB) // 切片的扩容
  • 扩容的触发逻辑,容量 < 长度+添加元素个数则 触发 growslice逻辑

  • 否则无需扩容,直接从len开始往后+1,赋值添加的元素

// cmd/compile/internal/walk/builtin.go
//   init {
//     s := src
//     const argc = len(args) - 1
//     if cap(s) - len(s) < argc {
//	    s = growslice(s, len(s)+argc)
//     }
//     n := len(s)
//     s = s[:n+argc]
//     s[n] = a
//     s[n+1] = b
//     ...
//   }
func walkAppend(n *ir.CallExpr, init *ir.Nodes, dst ir.Node) ir.Node {}  
  • growslice分析

num的初始长度和容量是 len=0,cap=0

0、所需的最小容量cap=1,len(nums)+len(1)

0、cap 大于 cap(nums)*2倍,则newcap=1

0、内存对齐的计算,capmem = roundupsize(uintptr(newcap) * sys.PtrSize)

0、capmem = roundupsize(1*8) = 8

0、所以最后所需容量的个数是1。newcap = int(capmem / sys.PtrSize), newcap=int(8/8)= 1

0、分配内存,将老数组内容copy到新分配好的数组中,返回新切片

2、为什么结果是这样?

下面代码执行是不会发生扩容的,已经提交预分配好容量了。

func TestSliceA(t *testing.T) {
	numA := make([]string, 0, 5)
	numA = append(numA, "a1", "a2")

  numsB := append(numA, "b1") // numsB = numA[:len(numA)+1] , numsB[len(nimA)] = "b1"
	t.Logf("numA=%v numB=%v\n", numA, numsB)

	numA = append(numA, "b2")
	t.Logf("numA=%v numB=%v \n", numA, numsB)
}

/*
    slice_test.go:15: numA=[a1 a2] numB=[a1 a2 b1]
    slice_test.go:18: numA=[a1 a2 b2] numB=[a1 a2 b2] 

*/
  • numA = append(numA,"a1","a2")

numA初始化容量=5,长度=0,并添加【a1,a2】元素

  • numsB := append(numA, "b1")

此时,numA 和numB的都指向了相同的底层数组,不一样就是,两者之间的长度不一样。即能够访问的元素的长度不同。

  • numA = append(numA, "b2")

因为numA和numB,共享底层数组,所以在numA添加b2是,将 numsB[2]覆盖掉。

Q&A

1、切片中的_type中的 size 和prtdata是什么?

size:切片类型大小

ptrdata:切片类型中指针的大小

主要是用来判断,分配合适的内存大小。

type _type struct {
	size       uintptr 
	ptrdata    uintptr // size of memory prefix holding all pointers
  // 代码省略。。。
}  

比如:

	type User struct {
		Age  *int
		Year int
	}
	nums := make([]User, 1)

size:16,ptrdata:8

	type User struct {
		Age  *int
		Year *int
	}
	nums := make([]*User, 1)

size:8,ptrdata:16

2、切片的容量的作用

如果不提前指定容量或者长度,则切片可能会发生频繁的,内存分好和值拷贝,影响效率。

用途:在数据拷贝和内存申请消耗与内存占用之间提供一个权衡。

3、slice的growslice调用方

growslice是指有发生扩容在会走这个方法

在执行append的时候,growslice的触发条件是 cap(slice) < len(slice)+ addEleCnt, 切片容量 < 切片长度+ 添加元素个数

注意注释中init的操作

// cmd/compile/internal/walk/builtin.go

//   init { 伪代码
//     s := src
//     const argc = len(args) - 1
//     if cap(s) - len(s) < argc {
//	    s = growslice(s, len(s)+argc)
//     }
//     n := len(s)
//     s = s[:n+argc]
//     s[n] = a
//     s[n+1] = b
//     ...
//   }
func walkAppend(n *ir.CallExpr, init *ir.Nodes, dst ir.Node) ir.Node {
  fn := typecheck.LookupRuntime("growslice") //   growslice(<type>, old []T, mincap int) (ret []T)
  

4、空切片和nil

  • nil切片:

只是声明不做任何初始化操作

var nums []string

底层内存结构的表现形式:slice的底层array是没有有地址

runtime.slice {array: unsafe.Pointer(0x0), len: 0, cap: 0}

使用场景:用来描述一个不存在的切片

  • 空切片,

make或者字面量初始化为空

// make初始化
nums := make([]string,0)

// 字面量初始化
nums2 := []string{}

底层内存结构的表现形式:slice的底层array是有地址

runtime.slice {array: unsafe.Pointer(0xc000040750), len: 0, cap: 0}

使用场景:用来表示空集合是很有用,比如数据库查询,没有查询到数据。

  • 使用

两者在使用append,len,cap,都不影响。