Golang中slice的实现原理

前言

简单来说,切片就是一种简化版的动态数组。因为动态数组的长度是不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,因此在Go代码中数组使用的并不多。而切片则使用得相当广泛。

Slice源码分析

注意:我会把源码中每个方法的作用都注释出来,可以参考注释进行理解。

在看源码之前,我们先来看一个简单的例子

func main() {
    
    
	slice:=make([]int,0) //第6行
	slice=append(slice,1)//第7行
}

通过make创建一个长度为0,容量为0的slice,接着通过append函数向slice追加元素
接下来,我们就来分析slice创建以及追加的过程
由于append() 是内置函数,所以我们只能通过查看它的汇编语言才能知道append() 发生了什么

内置函数:Go 语言拥有一些不需要进行导入操作就可以使用的内置函数

用以下命令查看的Go语言程序对应的伪汇编代码:

go tool compile -S slice.go

其中go tool compile命令用于调用Go语言提供的底层命令工具,其中-S参数表示输出汇编格式。

		...
        0x0028 00040 (slice.go:6)       PCDATA  $2, $1
        0x0028 00040 (slice.go:6)       PCDATA  $0, $0
        0x0028 00040 (slice.go:6)       LEAQ    type.int(SB), AX
        0x002f 00047 (slice.go:6)       PCDATA  $2, $0
        0x002f 00047 (slice.go:6)       MOVQ    AX, (SP)
        0x0033 00051 (slice.go:6)       XORPS   X0, X0
        0x0036 00054 (slice.go:6)       MOVUPS  X0, 8(SP)
        0x003b 00059 (slice.go:6)       CALL    runtime.makeslice(SB)
        0x0040 00064 (slice.go:6)       PCDATA  $2, $1
        0x0040 00064 (slice.go:6)       MOVQ    24(SP), AX
        0x0045 00069 (slice.go:7)       PCDATA  $2, $2
        0x0045 00069 (slice.go:7)       LEAQ    type.int(SB), CX
        0x004c 00076 (slice.go:7)       PCDATA  $2, $1
        0x004c 00076 (slice.go:7)       MOVQ    CX, (SP)
        0x0050 00080 (slice.go:7)       PCDATA  $2, $0
        0x0050 00080 (slice.go:7)       MOVQ    AX, 8(SP)
        0x0055 00085 (slice.go:7)       XORPS   X0, X0
        0x0058 00088 (slice.go:7)       MOVUPS  X0, 16(SP)
        0x005d 00093 (slice.go:7)       MOVQ    $1, 32(SP)
        0x0066 00102 (slice.go:7)       CALL    runtime.growslice(SB)
        0x006b 00107 (slice.go:7)       PCDATA  $2, $1
        0x006b 00107 (slice.go:7)       MOVQ    40(SP), AX
        0x0070 00112 (slice.go:7)       MOVQ    48(SP), CX
        0x0075 00117 (slice.go:7)       MOVQ    56(SP), DX
        0x007a 00122 (slice.go:7)       MOVQ    $1, (AX)
		...

其中,我们只需要关注CALL指令,看它调用了什么。

 0x003b 00059 (slice.go:6)       CALL    runtime.makeslice(SB)

slice.go第六行,调用了makeslice函数,则makeslice函数是用于初始化slice的

0x0066 00102 (slice.go:7)       CALL    runtime.growslice(SB)

同理,growslice函数是用于追加元素的
makeslice
接下来,我们看看在go的sdk里面slice是如何实现这些函数的

初始化makeslice

runtime\slice.go

type slice struct {
    
    
	array unsafe.Pointer
	len   int
	cap   int
}

slice 的底层结构定义非常直观,指向底层数组的指针,当前长度 len 和当前 slice 的 cap。
再来看看makeslice函数

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.
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
    
    
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}
	// 分配内存 
  	// 小对象从当前P 的cache中空闲数据中分配
  	// 大的对象 (size > 32KB) 直接从heap中分配
	return mallocgc(mem, et, true)
}

实在是太简单了,没啥可说的。mallocgc 函数会根据申请的内存大小,去对应的内存块链表上找合适的内存来进行分配,是 Go 自己改造的 tcmalloc 那一套。

数据扩容growslice

func growslice(et *_type, old slice, cap int) slice {
    
    
	//扫描内存
	if raceenabled {
    
    
		callerpc := getcallerpc()
		racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
	}
	if msanenabled {
    
    
		msanread(old.array, uintptr(old.len*int(et.size)))
	}
	//容量判断
	if cap < old.cap {
    
    
		panic(errorString("growslice: cap out of range"))
	}
	//如果存储的类型空间为0,  比如说 []struct{}, 数据为空,长度不为空
	if et.size == 0 {
    
    
		// append should not create a slice with nil pointer but non-zero len.
		// We assume that append doesn't need to preserve old.array in this case.
		return slice{
    
    unsafe.Pointer(&zerobase), old.len, cap}
	}

	newcap := old.cap
	doublecap := newcap + newcap
	//如果新的容量大于旧的两倍,则直接扩容到新的容量
	if cap > doublecap {
    
    
		newcap = cap
	} else {
    
    
	//当新的容量不大于旧的两倍
		// 如果旧长度小于1024,那扩容到旧的两倍
		if old.len < 1024 {
    
    
			newcap = doublecap
		} else {
    
    
			//否则扩容到旧的1.25倍
			for 0 < newcap && newcap < cap {
    
    
				newcap += newcap / 4
			}
			//检查newCap是否溢出
			if newcap <= 0 {
    
    
				newcap = cap
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// 为了加速计算
    // 对于不同的slice元素大小,选择不同的计算方法
    // 获取需要申请的内存大小。
	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)
	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)
	}

	// 判断是否会溢出
	if overflow || capmem > maxAlloc {
    
    
		panic(errorString("growslice: cap out of range"))
	}

	var p unsafe.Pointer
	if et.kind&kindNoPointers != 0 {
    
    
		//为新的切片开辟容量为capmem的地址空间
		p = mallocgc(capmem, nil, false)
		// 仅清除不会被覆盖的部分。
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
    
    
		// 注意:不能使用rawmem(这样可以避免内存清零),因为GC可以扫描未初始化的内存。
		p = mallocgc(capmem, et, true)
		if writeBarrier.enabled {
    
    
			//因为我们知道目标切片p,所以仅将指针隐藏在old.array中
			//仅包含nil指针,因为在分配过程中已将其清除。
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
		}
	}
	//数据拷贝
	memmove(p, old.array, lenmem)

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

扩容规则:

  1. 新的容量大于旧的2倍,直接扩容至新的容量
  2. 当新的容量不大于旧的2倍
    1. 当旧的长度小于1024,扩容至旧的2倍
    2. 扩容至旧的1.25倍

slice 扩容必然会导致内存拷贝,如果是性能敏感的系统中,尽可能地提前分配好 slice 是较好的选择。

数据拷贝slicecopy

func slicecopy(to, fm slice, width uintptr) int {
    
    
	//从slice长度为0或者到slice长度为0
	if fm.len == 0 || to.len == 0 {
    
    
		return 0
	}

	n := fm.len
	if to.len < n {
    
    
		n = to.len
	}
	//长度为0,直接返回
	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)))
	}
	
	size := uintptr(n) * width
	if size == 1 {
    
     
		// 直接内存拷贝
		*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
	} else {
    
    
		memmove(to.array, fm.array, size)
	}
	return n
}

总结

  • 尽量对切片设置初始容量值以避免扩容,slice 扩容必然会导致内存拷贝
  • 切片是一个结构体,保存着切片的容量,实际长度以及数组的地址
  • 多个slice 可能指向同一个底层数组
  • slice 是按值传递的,Go里只有传值(值传递)

猜你喜欢

转载自blog.csdn.net/xzw12138/article/details/107225548