Golang底层原理剖析之slice类型与扩容机制

概述

前言

  • 熟悉 slice 的底层数据结构 - 实际存储数据的array,当前长度len与容量cap
  • slice的扩容机制 - 不严格来说,当长度小于1024时,cap翻倍;大于1024时,增加1/4
  • slice 有很多特性与 map 一致 - 记住一点,代码中操作的slice和map只是上层的,实际存储数据的是array与hmap

通过代码学习底层

package main

import (
	"fmt"
	"unsafe"
)

func slice() {
	fmt.Println("Slice Init")
	var s []int
	// Tip: 对比一下map和slice的make函数,前者在类型后可跟0个或1个参数,而后者是1个和2个参数
	s = make([]int, 2)
	fmt.Println(len(s), cap(s))
	s = make([]int, 2, 4)
	fmt.Println(len(s), cap(s))

	// Tip: 元素个数小于cap时,append不会改变cap,只会增加len
	fmt.Println("Slice Assign")
	s[0] = 1
	s[1] = 2
	s = append(s, 4)
	fmt.Println(len(s), cap(s))

	// Tip: 元素个数超过cap时,会进行扩容
	s = append(s, 8, 16)
	fmt.Println(len(s), cap(s))

	// Tip: Slice没有显式的删除语句
	fmt.Println("Slice Delete")
	s = append(s[0:1], s[2:]...)
	fmt.Println(s)

	fmt.Println("Slice Range")
	for i, v := range s {
		fmt.Println(i, v)
		fmt.Printf("%p %p\n", &i, &v)
	}
}

/*
	slice 的源码部分

	slice基础结构slice:
	包括保存数据的array、长度len与容量cap

	初始化函数makeslice:
	math.MulUintptr:根据元素大小和容量cap,计算所需的内存空间
	mallocgc: 分配内存, 32K作为一个临界值,小的分配在P的cache中,大的分配在heap堆中

	扩容growslice:
	当长度小于1024时,cap翻倍;大于1024时,增加1/4。 但这个并不是绝对的,会根据元素的类型尽心过一定的优化

	拷贝slicecopy:
	核心函数为memmove,from=>to移动size大小的数据,size为 元素大小 * from和to中长度较小的个数

	拷贝slicestringcopy:
	基本与上面类似,字符串的拷贝
*/

func sliceAddr() {
	fmt.Println("Part 1")
	var s = make([]int, 2, 2)
	s[0] = 1
	fmt.Println(unsafe.Pointer(&s[0]))
	s[1] = 2
	fmt.Println(unsafe.Pointer(&s[0]))
	// Tip: 扩容后,slice的array的地址会重新分配
	s = append(s, 3)
	fmt.Println(unsafe.Pointer(&s[0]))

	fmt.Println("Part 2")
	// Tip: a虽然是一个新的地址,但指向的array是和a一致的
	a := s[:2]
	fmt.Printf("%p %p\n", &s, &a)
	fmt.Println(unsafe.Pointer(&a[0]))
	a[0] = 2
	fmt.Println(a, s)
	// Tip: 如果要进行slice拷贝,使用copy方法
	b := make([]int, 2)
	copy(b, s)//copy会重新分配底层array的内存
	fmt.Printf("%p %p\n", &s, &b)
	fmt.Println(unsafe.Pointer(&b[0]))

	fmt.Println("Part 3")
	// Tip: sNil的array指向nil,而sEmpty的array指向一个内存地址
	var sNil []int//底层array指向nil
	var sEmpty = make([]int, 0)//底层array指向一个内存地址,但是这个内存地址没有分配空间给它
	fmt.Println(len(sNil), len(sEmpty), cap(sNil), cap(sEmpty))
}

详解

slice类型

在runtime下的slice.go可以看到

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

slice基础结构包括保存数据的array、长度len与容量cap,其底层是数组array
在这里插入图片描述
这里的ints[3]和ints[4]不能访问,否则属于越界访问,会引发panic

扩容机制

在runtime下的slice.go内有一个扩容growslice函数
扩容机制是

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
//growslice部分代码
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.cap < 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
		}
	}
}
//下面还有优化

根据元素的类型做一定的优化

比如新容量是3,int类型,则它需要申请24B的内存,此时它会向语言自身的内存管理模块去申请内存,而内存管理模块会提前向操作系统申请一批内存,分为常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大,且最接近规格的内存,可能这里内存管理模块分配给你了32B的内存,所以这个时候新容量变成4个了

//runtime下sizeclasses.go文件
go采用的是基于tcmalloc进行的内存分配,也就是go语言的内存管理模块。
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 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}

在这里插入图片描述
在这里插入图片描述

posted @ 2021-10-26 20:35  cheems~  阅读(186)  评论(0编辑  收藏  举报