Golang中切片的内存对齐规则

本例探究是在64位的Macos系统下。

先明确一下Golang的 切片内存扩展规则:

Go 1.18 及之后版本的切片扩容大致规则如下:

		1. 首先判断新所需容量(原长度 + 新增元素数量)是否大于原容量。若大于,则需要扩容。
		2.  若原容量小于 256,新容量通常会直接翻倍。
		3.  若原容量大于等于 256,新容量会按一定比例增长,这个比例会逐渐变小,大致按 newcap += (newcap + 3*threshold) / 4 规则增长(threshold 初始值为 256) 。
		4.  计算出的新容量还需要根据元素类型的大小进行内存对齐调整,最终得到实际分配的容量。

那到底是如何计算的呢?
看如下代码:

func slicefun() {
	var s1 []int
	s1 = append(s1, 1)
	s1 = append(s1, 2)
	s1 = append(s1, 3, 4, 5)
	printSlice(s1)
}
func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d s = %v\n", len(s), cap(s), s) // 输出切片的长度、容量和内容
	if len(s) > 0 {
		fmt.Println("First element:", s[0])       // 输出切片的第一个元素
		fmt.Println("Last element:", s[len(s)-1]) // 输出切片的最后一个元素
	}
	// 遍历切片
	for i, v := range s {
		fmt.Printf("Element at index %d: %d\n", i, v) // 输出切片每个元素
	}
	fmt.Println("----------------------------------")
}

输出结果如下:

len=5 cap=6 s = [1 2 3 4 5]
First element: 1
Last element: 5
Element at index 0: 1
Element at index 1: 2
Element at index 2: 3
Element at index 3: 4
Element at index 4: 5

按照增长规则,第三次append(s1,3,4,5)之前,cap是2,现在一次性增加三个元素,理论上应该是22=4为容量,但容量4又装不下(因为2+3个新元素=5)。应该再次翻倍,42 = 8.但实际上,Go只给了6。与预期不符合。So有了如下探究。

新代码如下:

func slicefun() {
	var s1 []int
	fmt.Printf("Initial: len=%d cap=%d s = %v, required memory: %d bytes\n", len(s1), cap(s1), s1, len(s1)*int(unsafe.Sizeof(int(0))))
	s1 = append(s1, 1)
	fmt.Printf("After append 1: len=%d cap=%d s = %v, required memory: %d bytes\n", len(s1), cap(s1), s1, len(s1)*int(unsafe.Sizeof(int(0))))
	s1 = append(s1, 2)
	fmt.Printf("After append 2: len=%d cap=%d s = %v, required memory: %d bytes\n", len(s1), cap(s1), s1, len(s1)*int(unsafe.Sizeof(int(0))))
	s1 = append(s1, 3, 4, 5)
	fmt.Printf("After append 3,4,5: len=%d cap=%d s = %v, required memory: %d bytes\n", len(s1), cap(s1), s1, len(s1)*int(unsafe.Sizeof(int(0))))
	printSlice(s1)
}

输出结果为:

Initial: len=0 cap=0 s = [], required memory: 0 bytes
After append 1: len=1 cap=1 s = [1], required memory: 8 bytes
After append 2: len=2 cap=2 s = [1 2], required memory: 16 bytes
After append 3,4,5: len=5 cap=6 s = [1 2 3 4 5], required memory: 40 bytes
len=5 cap=6 s = [1 2 3 4 5]
First element: 1
Last element: 5
Element at index 0: 1
Element at index 1: 2
Element at index 2: 3
Element at index 3: 4
Element at index 4: 5

可以看到,当最后一次新增后,占用了40个字节。这就意味着,为了内存对齐,将会分配48个字节空间给s1。So,48/8 = 6,即 cap=6

结论:

在 Go 语言里,内存对齐是一个重要概念,它能提升内存访问效率。CPU 访问内存时,通常按特定字节数块来读取,若数据地址符合对齐要求,访问速度会更快。下面详细解释在 slicefun 函数里切片扩容时的内存对齐规则。

内存对齐的基本概念
内存对齐指的是将数据存储在特定地址上,该地址是数据类型大小的整数倍。例如,在 64 位系统中,int 类型通常占 8 字节,那么 int 类型的数据就会被存储在地址是 8 的整数倍的位置上。这样做能减少 CPU 访问内存的次数,提高性能。

切片扩容中的内存对齐
在切片扩容时,Go 语言的运行时会根据元素类型的大小和所需的元素数量,结合内存分配器的规则来确定最终的容量。Go 的内存分配器会预先定义一系列固定大小的内存块,当需要分配内存时,会选择能满足需求的最小内存块。

结合 slicefun 函数分析

func slicefun() {
	var s1 []int
	s1 = append(s1, 1)
	s1 = append(s1, 2)
	s1 = append(s1, 3, 4, 5)
	printSlice(s1)
}

在执行 s1 = append(s1, 3, 4, 5) 前,切片 s1 的长度为 2,容量为 2。此次需要新增 3 个元素,所以新的所需元素数量为 5。

  1. 计算所需内存大小
    在 64 位系统中,int 类型占 8 字节。因此,存储 5 个 int 类型的元素需要 5 * 8 = 40 字节的内存。
  2. 查找合适的内存块
    Go 的内存分配器有一系列预定义的内存块大小,常见的如 8、16、32、48、64 字节等。运行时会从这些预定义的内存块中选择一个能容纳所需内存大小的最小内存块。由于 40 字节大于 32 字节,小于 48 字节,所以会选择 48 字节的内存块。
  3. 计算最终容量
    选择了 48 字节的内存块后,用这个内存块的大小除以元素类型的大小,就能得到最终的切片容量。即 48 / 8 = 6,所以切片的容量会被调整为 6。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值