简单说说go语言Slice的底层实现

go语言的Slice

slice的结构

type slice struct {
	array unsafe.Pointer	// 指向数组的指针
	len   int								// 切片长度
	cap   int							  // 切片容量
}

切片我的理解是,他是数组的一个片段,就是说他是数组上截取的一部分,它可以通过下标访问到具体的元素,也能在原数组中自由活动,当你往里面append新元素的时候,如果当前数组容量足够,还会修改原数组的值;如果容量不够了,它就会把原数组中的值拷贝到一个新的数组中(新数组的容量按扩容规则增加),然后新加的元素加入到新的数组中,此时切片的底层数组就不再是原来的数组了,这个时候,你修改切片中的值,原数组就不会发生变化,因为他们不再有关联了,只是对于使用者来说,我们察觉不到这个过程.

slice的创建

下面我们用几个普通的例子来试试创建slice

func createSlice()  {
	s := make([]int, 0)
	s = append(s, 1)
	fmt.Println(s)

	var s2 []int
	s2 = append(s2, 1)
	fmt.Println(s2)

	s3 := []int{}
	s3 = append(s3, 1)
	fmt.Println(s3)

}
/**
  输出结果为:
  [1]
  [1]
  [1]
 */

上面的例子还是很好理解的,那我们接下来看看这几种创建模式的差异:

go tool compile -S main.go使用上述命令,查看上面的代码在底层都做了些什么?

完整的返回我这边就不展示了,就展示关键的几行代码:

 0x0021 00033 (main.go:6)        LEAQ    type.int(SB), AX
        0x0028 00040 (main.go:6)        PCDATA  $0, $0
        0x0028 00040 (main.go:6)        MOVQ    AX, (SP)
        0x002c 00044 (main.go:6)        XORPS   X0, X0
        0x002f 00047 (main.go:6)        MOVUPS  X0, 8(SP)
        s := make([]int, 0)
        0x0034 00052 (main.go:6)        CALL    runtime.makeslice(SB)
		......
        0x003e 00062 (main.go:7)        LEAQ    type.int(SB), CX
		......
		s = append(s, 1)
        0x005f 00095 (main.go:7)        CALL    runtime.growslice(SB)
      	......
        0x008c 00140 (main.go:8)        CALL    runtime.convTslice(SB) 这个为fmt.Println的具体实现,这里就不多说了
       
      ============================================================ 
         0x00e7 00231 (main.go:11)       LEAQ    type.int(SB), AX
        ......
        var s2 []int
		0x019f 00415 (main.go:15)       LEAQ    runtime.zerobase(SB), AX
		......
		s2 = append(s2, 1)
        0x010c 00268 (main.go:11)       CALL    runtime.growslice(SB)
        ......
        0x0139 00313 (main.go:12)       CALL    runtime.convTslice(SB)

从上面我们不难看出,不同的切片创建方法,底层做了不同的操作第一个是runtime.makeslice,第二个没有对应的实现,第三个是runtime.zerobase,这三种方式区别还是有的,但是最终都可以实现我们的目标,因为最终都是调用了runtime.mallocgc 函数,也就是分配内存,看到这有同学就会问了,第二种方式我们并没有对应的实现啊,也就是说并没有分配内存啊。是的,第二种方式我们确实没有直接分配内存,并且我们直接append了,其实是因为append操作会对没有分配内存的切片再分配一下内存(这个我们下面会说到)。所以我们在写代码时,使用 var 关键字声明切片是完全可以的,并且对于长度为0的切片我们非常建议这样声明。

现在我们先看下runtime.makeslice:

// et为元素类型
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()
	}

	return mallocgc(mem, et, true)
}

// 首先程序会根据你元素的大小以及容量做个计算(mem=size*cap)以及判断是否溢出,如果溢出了,或者大于了最大可分配的内存大小(与操作系统位数有关),以及长度小于0或者长度小于容量,就会报错.然后下面会再去判断具体是长度错误还是容量错误

// return mallocgc(mem, et, true) 才是真正去分配内存

slice的扩容

网上关于切片的扩容都是什么切片容量小于1024的时候,新切片的容量翻倍,大于1024之后,新切片的容量为旧容量的1.25倍,但是实际上真的是这样的吗?我们先看一段代码:

slice := make([]int32,3,3)
	fmt.Println("len:", len(slice), "cap:", cap(slice))
	slice = append(slice, 1)
	fmt.Println("len:", len(slice), "cap:", cap(slice))
	fmt.Println(slice)

/**
	输出结果如下:
	len: 3 cap: 3
	len: 4 cap: 8
	[0 0 0 1]
	/
	

从上面的例子看好像并不是翻倍耶(我的同事说因为3+1=4 然后翻倍就变成了8,哈哈哈开个玩笑哈),那么这是怎么一回事呢?我们看看源码:

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"))
	}

	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 {
		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
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// Specialize for common values of et.size.
	// For 1 we don't need any division/multiplication.
	// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
	// For powers of 2, use a variable shift.
	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)
	}

	// The check of overflow in addition to capmem > maxAlloc is needed
	// to prevent an overflow which can be used to trigger a segfault
	// on 32bit architectures with this example program:
	//
	// type T [1<<27 + 1]int64
	//
	// var d T
	// var s []T
	//
	// func main() {
	//   s = append(s, d, d, d, d)
	//   print(len(s), "\n")
	// }
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: cap out of range"))
	}

	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)
		}
	}
	memmove(p, old.array, lenmem)

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

从这里我们可以看出好像的确是按网上说的规则进行的扩容:

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
			}
		}
	}

在这里我们拿到了新的cap应该是6才对是吧,但是为什么就变成了8呢?我们继续往下看:

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)
	}

它会根据你元素的类型从新给你计算一下你的新容量,这个时候得到的新容量才是你切片的真正容量,所以即使你没超过1024,也不一定就是翻倍了,而是根据你的元素类型的计算的.(int32是4个字节的元素,在这里4个字节的元素计算起来较为麻烦,这里就不多叙述了,下面我用一个int类型的元素详细说说)

我们接下来再看个例子:(我的电脑是64位)

	slice := make([]int64, 3,3)
	fmt.Println("len:", len(slice), "cap:", cap(slice)) // 3 3

	slice = append(slice, 1)
	fmt.Println("len:", len(slice), "cap:", cap(slice))// 4 6

	var a int64
	fmt.Println(unsafe.Sizeof(a)) // 8

这个时候就是翻倍了,我们按上面的说法计算一下试试:

  1. newcap := old.cap =>newcap = 3

  2. doublecap := newcap + newcap => doublecap = 6

  3. cap为新切片的容量即:旧切片容量3 + 新append的1个元素=4

  4. cap < doublecap && old.len < 1024所以newcap = doublecap => newcap = 6

  5. 由上我们可知int64为8位,我们现在再来这个switch语句中看看

  6. 此时我们进入的是case et.size == sys.PtrSize这个语句里(这个sys.PtrSize与你的系统位数有关,我这边得到的值是8)

  7. lenmemnewlenmem我们可以先不关心,这个是用于把原切片中的元素拷贝到新的切片中去而使用的

  8. capmem = roundupsize(uintptr(newcap) * sys.PtrSize),这个roundupsize其实是一个内存对齐的过程

    go中分配内存的规则如下(由源码我们可知roundupsize的传参是6*8=48,刚好有对应的48与之对应):

    // class  bytes/obj  bytes/span  objects  tail waste  max waste
    //     1          8        8192     1024           0     87.50%
    //     2         16        8192      512           0     43.75%
    //     3         32        8192      256           0     46.88%
    //     4         48        8192      170          32     31.52%
    //     5         64        8192      128           0     23.44%
    //     6         80        8192      102          32     19.07%
    //     7         96        8192       85          32     15.95%
    //     8        112        8192       73          16     13.56%
    //     9        128        8192       64           0     11.72%
    //    10        144        8192       56         128     11.82%
    //    11        160        8192       51          32      9.73%
    //    12        176        8192       46          96      9.59%
    //    13        192        8192       42         128      9.25%
    //    14        208        8192       39          80      8.12%
    //    15        224        8192       36         128      8.15%
    //    16        240        8192       34          32      6.62%
    //    17        256        8192       32           0      5.86%
    //    18        288        8192       28         128     12.16%
    //    19        320        8192       25         192     11.80%
    //    20        352        8192       23          96      9.88%
    //    21        384        8192       21         128      9.51%
    //    22        416        8192       19         288     10.71%
    //    23        448        8192       18         128      8.37%
    //    24        480        8192       17          32      6.82%
    //    25        512        8192       16           0      6.05%
    //    26        576        8192       14         128     12.33%
    //    27        640        8192       12         512     15.48%
    //    28        704        8192       11         448     13.93%
    //    29        768        8192       10         512     13.94%
    //    30        896        8192        9         128     15.52%
    //    31       1024        8192        8           0     12.40%
    //    32       1152        8192        7         128     12.41%
    //    33       1280        8192        6         512     15.55%
    //    34       1408       16384       11         896     14.00%
    //    35       1536        8192        5         512     14.00%
    //    36       1792       16384        9         256     15.57%
    //    37       2048        8192        4           0     12.45%
    //    38       2304       16384        7         256     12.46%
    //    39       2688        8192        3         128     15.59%
    //    40       3072       24576        8           0     12.47%
    //    41       3200       16384        5         384      6.22%
    //    42       3456       24576        7         384      8.83%
    //    43       4096        8192        2           0     15.60%
    //    44       4864       24576        5         256     16.65%
    //    45       5376       16384        3         256     10.92%
    //    46       6144       24576        4           0     12.48%
    //    47       6528       32768        5         128      6.23%
    //    48       6784       40960        6         256      4.36%
    //    49       6912       49152        7         768      3.37%
    //    50       8192        8192        1           0     15.61%
    //    51       9472       57344        6         512     14.28%
    //    52       9728       49152        5         512      3.64%
    //    53      10240       40960        4           0      4.99%
    //    54      10880       32768        3         128      6.24%
    //    55      12288       24576        2           0     11.45%
    //    56      13568       40960        3         256      9.99%
    //    57      14336       57344        4           0      5.35%
    //    58      16384       16384        1           0     12.49%
    //    59      18432       73728        4           0     11.11%
    //    60      19072       57344        3         128      3.57%
    //    61      20480       40960        2           0      6.87%
    //    62      21760       65536        3         256      6.25%
    //    63      24576       24576        1           0     11.45%
    //    64      27264       81920        3         128     10.00%
    //    65      28672       57344        2           0      4.91%
    //    66      32768       32768        1           0     12.50%
    

    capmem最后返回的值是48,因此newcap = 48 / 8= 6

有的盆友可能就会说了,你这个用例没有说服力啊,他还是翻倍了呀!,好,我给你举一个不是翻倍的例子:

slice := []int64{1,2}
	fmt.Println("len:", len(slice), "cap:", cap(slice)) // 2 2

	slice = append(slice, 3)
	fmt.Println("len:", len(slice), "cap:", cap(slice)) // 3 4

	slice = append(slice, 4)
	fmt.Println("len:", len(slice), "cap:", cap(slice)) // 4 4

	slice = append(slice, 5)
	fmt.Println("len:", len(slice), "cap:", cap(slice)) // 5 8

	var a int64
	fmt.Println(unsafe.Sizeof(a)) // 8

这个咋一看没问题,都翻倍了是吧,我们接着往下看:

	slice := []int64{1,2}
	fmt.Println("len:", len(slice), "cap:", cap(slice)) // 2 2

	slice = append(slice, 3,4,5)
	fmt.Println("len:", len(slice), "cap:", cap(slice)) // 5 6

这又是什么鬼???

我们接着往下看源码:

newcap := old.cap // 2
	doublecap := newcap + newcap // 4
// cap = 2+3 = 5
	if cap > doublecap {
		newcap = cap
	}
此时newcap = 5

然后计算capmem:
capmem = roundupsize(uintptr(newcap) * sys.PtrSize) // uintptr(newcap) * sys.PtrSize = 5 * 8 =40
根据上面内存分配的规则,可知32 < 40 < 48
此时向上取整,实际分配内存为48
所以newcap = int(capmem / sys.PtrSize) = 48/8 = 6

当然go的内存分配没这么简单,这里只讲述与slice相关的部分,至此你应该知道slice的扩容的基本原理了吧

slice的截取

我们还要注意的是,扩容时如果容量大于原有数据的长度,我们重新分配内存,其操作不会影响原有的数据。但是没有分配新的内存,也就是说还是原来数组的基础上添加元素,那么新的切片操作就会影响原有的数组。这部分依然不再赘述,看一下下面代码,大家都明白了:

	s := []int{1,2,3,4,5,6}
	fmt.Println(len(s), cap(s))  // 6 6

	s2 := s[1:3]
	fmt.Println(len(s2), cap(s2))// 2 5
	这个时候s2的底层数组是截取的s的一部分,所以虽然他的长度是2,但是容量是5(从第二个元素开始)

	s3 := s2[1:]
	fmt.Println(len(s3), cap(s3)) // 1 4
	这个时候s3又是截取的s2的一部分,但是他们底层数组是同一个,所以只是在s2的基础上后移了一个元素
	
	s3[0] = 9
	fmt.Println(s)  // [1 2 9 4 5 6]    这个时候,s3的第一个元素是s的第三个元素,所以s数组改变了
	s3 = append(s3, 7)
	fmt.Println(s)  // [1 2 9 7 5 6] s3的容量足够,不扩容,因此s3的后一位append了7,所以s的第4位变成了7
	fmt.Println(s3) // [9 7] 
	
	s3 = append(s3,8,7,10,11) 此时,s3的容量不足,发生了扩容,底层数组发生了改变
	fmt.Println(s) // [1 2 9 7 5 6] 底层数组不再改动
	fmt.Println(s3) // [9 7 8 7 10 11] s3正常append了新元素

slice的拷贝

切片的拷贝分为2种,一种是浅拷贝,一种是深拷贝;浅拷贝这边就不多说了,感兴趣的可以自行学习;我们重点说一下切片的深拷贝,这里我们需要使用切片内置的一个方法copy

	s := []int{1, 2, 3, 4}
	var s1 []int
	copy(s1, s)
	fmt.Println(s1) // []
	fmt.Println(s) // [1 2 3 4]

	s1 = make([]int, 2)
	count := copy(s1, s)
	fmt.Println(s1) // [1 2]
	fmt.Println(s) // [1 2 3 4]
	fmt.Println(count) // 2

	s1[0] = 5
	fmt.Println(s1) // [5 2]
	fmt.Println(s) // [1 2 3 4]

从上面可知,copy 只会拷贝目标切片长度个元素,并且 copy 后两个切片是没有影响的。

你可能感兴趣的:(golang)