目录
例3:使用append逐个添加元素和一次性添加多个元素的区别
例5:当容量大于1024的时候,每次扩容真的是1.25倍吗?
本文是对上一篇文章的补充:
go slice切片的详细知识(包含底层扩容)-CSDN博客
例子
例3:使用append逐个添加元素和一次性添加多个元素的区别
一次性添加多个元素:slice = append(slice, 1, 2, 3)
- 性能:可能更高效,因为只进行了一次内存分配和复制操作。
- 代码简洁:代码更简洁明了。
逐个添加元素:slice = append(slice, 1)、slice = append(slice, 2)、slice = append(slice, 3)
- 性能:可能较低,因为每次添加元素时,切片可能需要多次进行内存分配和复制操作(取决于底层数组的容量)。
- 内存分配:可能会多次触发内存分配操作。
- 代码冗长:代码较为冗长,不如一次性添加多个元素的方式简洁。
Go
// 示例 1: 一次性添加多个元素
var s1 []int
s1 = append(s1, 1, 2, 3)
fmt.Printf("%p %v %d %d\n", s1, s1, len(s1), cap(s1)) // 0xc000020090 [1 2 3] 3 3
var slice1 []int = []int{1, 2}
fmt.Printf("%p %v %d %d\n", slice1, slice1, len(slice1), cap(slice1)) // 0xc000128010 [1 2] 2 2
slice1 = append(slice1, 3, 4, 5)
fmt.Printf("%p %v %d %d\n", slice1, slice1, len(slice1), cap(slice1)) // 0xc000120060 [1 2 3 4 5] 5 6
// 示例 2: 逐个添加元素
var slice2 []int = []int{1, 2}
fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc000128060 [1 2] 2 2
slice2 = append(slice2, 3)
fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012c020 [1 2 3] 3 4
slice2 = append(slice2, 4)
fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012c020 [1 2 3 4] 4 4
slice2 = append(slice2, 5)
fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012e000 [1 2 3 4 5] 5 8
内容最终相同,但它们的内存分配情况可能不同。多次调用 append 可能会导致多次内存分配,而一次性添加多个元素则可能只需要进行一次内存分配和复制。
例4:order[low:high:max]
切片在被截取时的另一个特点是,被截取后的数组仍然指向原始切片的底层数据。
如:bar 执行了 append 函数之后,最终也修改了 foo 的最后一个元素,这是一个在实践中非常常见的陷阱。
Go
foo := []int{0, 0, 0, 42, 100}
bar := foo[1:4]
fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
fmt.Println(len(bar), cap(bar), bar) // 3 4 [0 0 42]
fmt.Printf("%p %p\n", foo, bar) // 0xc00001c1b0 0xc00001c1b8 虽然地址不同(切片结构体还有长度和容量属性,所以切片结构体地址不同),但是指向的底层数组是同一个,因为没有扩容
bar = append(bar, 99)
fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 99]
fmt.Println(len(bar), cap(bar), bar) // 4 4 [0 0 42 99]
如果要解决这样的问题,其实可以在截取时指定容量:order[low:high:max]
Go
foo := []int{0, 0, 0, 42, 100}
bar := foo[1:4:4]
// bar := foo[1:4:3] // 报错:Invalid index values, must be low <= high <= max
fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
fmt.Println(len(bar), cap(bar), bar) // 3 3 [0 0 42]
fmt.Printf("%p %p\n", foo, bar) // 0xc00001c1b0 0xc00001c1b8
bar = append(bar, 99)
fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
fmt.Println(len(bar), cap(bar), bar) // 4 6 [0 0 42 99]
解释foo[1:4:4]:
- low 是 1,表示新切片从 foo 的索引 1 开始(包含这个元素)。
- high 是 4,表示新切片到 foo 的索引 4 结束(不包含这个元素)。
- max 是 4,表示新切片的容量是从 low 开始到 max 结束的长度。
练习:
Go
sliceA := make([]int, 5, 10)
sliceB := sliceA[0:5]
sliceC := sliceA[0:5:5]
fmt.Println(len(sliceA), cap(sliceA)) // 5 10
fmt.Println(len(sliceB), cap(sliceB)) // 5 10
fmt.Println(len(sliceC), cap(sliceC)) // 5 5
Go
orderLen := 5
order := make([]uint16, 2*orderLen)
pollorder := order[:orderLen:orderLen]
lockorder := order[orderLen:][:orderLen:orderLen]
// pollorder切片指的是order的前半部分切片,lockorder指的是order的后半部分切片,即原order分成了两段。所以,pollorder和lockerorder的长度和容量都是5。
fmt.Println(len(pollorder), cap(pollorder)) // 5 5
fmt.Println(len(lockorder), cap(lockorder)) // 5 5
Go
sli := make([]int, 0)
sli = append(sli, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}...)
s := sli[5:][:5] // sli 和 s 共享同一个底层数组
// sli[5:]:[6, 7, 8, 9, 10]
// [:5]:从上述新切片中再取前5个元素,结果是[6, 7, 8, 9, 10]
fmt.Println(s) // [6 7 8 9 10]
s[0] = 111
fmt.Println(s, sli) // [111 7 8 9 10] [1 2 3 4 5 111 7 8 9 10]
例5:当容量大于1024的时候,每次扩容真的是1.25倍吗?
1024 直接扩容到 1536,不是1.25倍,而是1.5倍,这是为什么?(对于容量大于等于 1024 的切片,在扩容时 Go 并不是简单地按照1.25倍扩容,而是使用了一种更复杂的策略。)
Go
s2 := make([]int, 1024)
fmt.Printf("s2: len: %d, cap: %d\n", len(s2), cap(s2)) // s2: len: 1024, cap: 1024
s2 = append(s2, 1)
fmt.Printf("s2: len: %d, cap: %d\n", len(s2), cap(s2)) // s2: len: 1025, cap: 1536
向 slice 追加元素的时候,若容量不够,会调用 growslice 函数:
Go
func growslice(et *_type, old slice, cap int) slice {
// ......
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
// ......
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}
for循环:会不断循环直到 newcap 超过所需的容量。最终的结果可能会比严格的1.25倍略大一些,具体取决于当前的容量和内存分配的优化策略。
最后两行代码:对 newcap 作了一个内存对齐,这个和内存分配策略相关,所以最终结果不一定是 1.25的整数倍(有时候扩容和元素类型的字节数有关系)。
Go 语言的切片扩容机制是相当复杂的,它考虑了多种因素来确定新的容量,以便在性能和内存使用之间找到平衡。Go 语言的 runtime 库在执行切片扩容时,有时会为了减少频繁的内存分配而使用稍大的倍数。这种优化主要是为了减少内存分配次数,提高性能。
Go 语言中切片扩容的策略为:
- 如果新申请容量(cap)大于旧容量(old.cap)的两倍,则最终容量(newcap)是新申请的容量(cap);
- 如果旧切片的长度小于 1024,则最终容量是旧容量的 2 倍,即"newcap=doublecap";
- 如果旧切片的长度大于或等于 1024,则最终容量从旧容量开始循环增加原来的 1/4,直到最终容量大于或等于新申请的容量为止;
- 如果最终容量计算值溢出,即超过了 int 的最大范围,则最终容量就是新申请容量。