Go 언어 슬라이싱은 어떻게 확장되나요? 다음 글에서는 Go 언어의 슬라이스 확장 메커니즘을 소개하겠습니다. 도움이 되길 바랍니다.
Go 언어에는 매우 일반적으로 사용되는 데이터 구조, 즉 슬라이스가 있습니다.
슬라이스는 동일한 유형의 요소로 구성된 가변 길이 시퀀스입니다. 배열 유형을 기반으로 하는 캡슐화 계층입니다. 매우 유연하며 자동 확장을 지원합니다.
슬라이스(Slice)는 Pointer, Length 및 Capacity라는 세 가지 속성을 갖는 참조 유형입니다.
기본 소스 코드는 다음과 같이 정의됩니다.
type slice struct { array unsafe.Pointer len int cap int }
예를 들어 make([]byte, 5)
를 사용하여 다음과 같은 슬라이스를 생성합니다. make([]byte, 5)
创建一个切片,它看起来是这样的:
切片的使用还是比较简单的,这里举一个例子,直接看代码吧。
func main() { var nums []int // 声明切片 fmt.Println(len(nums), cap(nums)) // 0 0 nums = append(nums, 1) // 初始化 fmt.Println(len(nums), cap(nums)) // 1 1 nums1 := []int{1,2,3,4} // 声明并初始化 fmt.Println(len(nums1), cap(nums1)) // 4 4 nums2 := make([]int,3,5) // 使用make()函数构造切片 fmt.Println(len(nums2), cap(nums2)) // 3 5 }
当切片的长度超过其容量时,切片会自动扩容。这通常发生在使用 append
函数向切片中添加元素时。
扩容时,Go 运行时会分配一个新的底层数组,并将原始切片中的元素复制到新数组中。然后,原始切片将指向新数组,并更新其长度和容量。
需要注意的是,由于扩容会分配新数组并复制元素,因此可能会影响性能。如果你知道要添加多少元素,可以使用 make
函数预先分配足够大的切片来避免频繁扩容。
接下来看看 append
函数,签名如下:
func Append(slice []int, items ...int) []int
append
函数参数长度可变,可以追加多个值,还可以直接追加一个切片。使用起来比较简单,分别看两个例子:
追加多个值:
package main import "fmt" func main() { s := []int{1, 2, 3} fmt.Println("初始切片:", s) s = append(s, 4, 5, 6) fmt.Println("追加多个值后的切片:", s) }
输出结果为:
初始切片: [1 2 3] 追加多个值后的切片: [1 2 3 4 5 6]
再来看一下直接追加一个切片:
package main import "fmt" func main() { s1 := []int{1, 2, 3} fmt.Println("初始切片:", s1) s2 := []int{4, 5, 6} s1 = append(s1, s2...) fmt.Println("追加另一个切片后的切片:", s1) }
输出结果为:
初始切片: [1 2 3] 追加另一个切片后的切片: [1 2 3 4 5 6]
再来看一个发生扩容的例子:
package main import "fmt" func main() { s := make([]int, 0, 3) // 创建一个长度为0,容量为3的切片 fmt.Printf("初始状态: len=%d cap=%d %v\n", len(s), cap(s), s) for i := 1; i <= 5; i++ { s = append(s, i) // 向切片中添加元素 fmt.Printf("添加元素%d: len=%d cap=%d %v\n", i, len(s), cap(s), s) } }
输出结果为:
初始状态: len=0 cap=3 [] 添加元素1: len=1 cap=3 [1] 添加元素2: len=2 cap=3 [1 2] 添加元素3: len=3 cap=3 [1 2 3] 添加元素4: len=4 cap=6 [1 2 3 4] 添加元素5: len=5 cap=6 [1 2 3 4 5]
在这个例子中,我们创建了一个长度为 0
,容量为 3
的切片。然后,我们使用 append
函数向切片中添加 5
个元素。
当我们添加第 4
个元素时,切片的长度超过了其容量。此时,切片会自动扩容。新的容量是原始容量的两倍,即 6
。
表面现象已经看到了,接下来,我们就深入到源码层面,看看切片的扩容机制到底是什么样的。
在 Go 语言的源码中,切片扩容通常是在进行切片的 append
操作时触发的。在进行 append
操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice
函数进行扩容。
growslice
函数定义在 Go 语言的 runtime 包中,它的调用是在编译后的代码中实现的。具体来说,当执行 append
操作时,编译器会将其转换为类似下面的代码:
slice = append(slice, elem)
在上述代码中,如果切片容量不足以容纳新的元素,则会调用 growslice
函数进行扩容。所以 growslice
函数的调用是由编译器在生成的机器码中实现的,而不是在源代码中显式调用的。
切片扩容策略有两个阶段,go1.18 之前和之后是不同的,这一点在 go1.18 的 release notes 中有说明。
下面我用 go1.17 和 go1.18 两个版本来分开说明。先通过一段测试代码,直观感受一下两个版本在扩容上的区别。
package main import "fmt" func main() { s := make([]int, 0) oldCap := cap(s) for i := 0; i < 2048; i++ { s = append(s, i) newCap := cap(s) if newCap != oldCap { fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) oldCap = newCap } } }
上述代码先创建了一个空的 slice,然后在一个循环里不断往里面 append
슬라이싱의 사용법은 비교적 간단합니다. 코드를 직접 살펴보겠습니다.
[0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 1024 [0 -> 1023] cap = 1024 | after append 1024 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1696 [0 -> 1695] cap = 1696 | after append 1696 cap = 2304
append
함수를 사용하여 슬라이스에 요소를 추가할 때 발생합니다. 확장 시 Go 런타임은 새로운 기본 배열을 할당하고 원본 슬라이스의 요소를 새 배열에 복사합니다. 그러면 원래 슬라이스는 길이와 용량이 업데이트된 새 어레이를 가리키게 됩니다.
확장은 새로운 배열을 할당하고 요소를 복사하므로 성능에 영향을 미칠 수 있다는 점에 유의해야 합니다
. 추가하려는 요소 수를 알고 있는 경우make
함수를 사용하여 빈번한 확장을 피할 수 있을 만큼 큰 조각을 미리 할당할 수 있습니다. 🎜🎜append
함수를 살펴보겠습니다. 서명은 다음과 같습니다. 🎜[0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 848 [0 -> 847] cap = 848 | after append 848 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1792 [0 -> 1791] cap = 1792 | after append 1792 cap = 2560
append
함수 매개변수의 길이는 가변적이며 여러 값이 가능합니다. 추가하거나 슬라이스를 직접 추가할 수 있습니다. 두 가지 예를 각각 살펴보겠습니다. 🎜🎜🎜여러 값 추가: 🎜🎜// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... 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 } } } // ... return slice{p, old.len, newcap} }
// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. 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 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } // ... return slice{p, old.len, newcap} }
capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize)
0
이고 용량이 3
조각. 그런 다음 append
함수를 사용하여 5
요소를 슬라이스에 추가합니다. 🎜🎜 4
요소를 추가하면 슬라이스 길이가 용량을 초과합니다. 이때 슬라이스가 자동으로 확장됩니다. 새로운 용량은 원래 용량인 6
의 두 배입니다. 🎜🎜피상적인 현상을 살펴보았습니다. 다음으로 슬라이싱 확장 메커니즘이 어떤 모습인지 알아보기 위해 소스 코드 수준으로 자세히 살펴보겠습니다. 🎜append
작업을 수행할 때 슬라이스 확장이 트리거됩니다. 일부분. append
작업 중에 슬라이스 용량이 새 요소를 수용하기에 충분하지 않으면 슬라이스를 확장해야 합니다. 이때 growslice
함수가 호출되어 확장됩니다. 용량. 🎜🎜growslice
함수는 Go 언어의 런타임 패키지에 정의되어 있으며 해당 호출은 컴파일된 코드에서 구현됩니다. 구체적으로 append
작업이 수행되면 컴파일러는 이를 다음과 유사한 코드로 변환합니다. 🎜rrreee🎜위 코드에서 슬라이스 용량이 새 요소를 수용할 만큼 충분하지 않으면 확장을 위해 growslice
함수가 호출됩니다. 따라서 growslice
함수에 대한 호출은 소스 코드에서 명시적으로 호출되는 것이 아니라 생성된 기계어 코드에서 컴파일러에 의해 구현됩니다. 🎜🎜슬라이스 확장 전략에는 go1.18 이전과 이후가 다른 두 단계가 있습니다. 이에 대해서는 go1.18 릴리스 노트에 설명되어 있습니다. 🎜🎜아래에서는 go1.17과 go1.18 버전을 사용하여 별도로 설명하겠습니다. 먼저, 두 버전 간의 확장 차이를 직관적으로 느껴보기 위해 테스트 코드를 살펴보겠습니다. 🎜rrreee🎜위 코드는 먼저 빈 슬라이스를 만든 다음 루프에서 새 요소를 해당 슬라이스에 추가
합니다. 🎜🎜그런 다음 용량이 변경될 때마다 기존 용량과 추가된 요소, 요소를 추가한 후의 용량을 기록합니다. 🎜🎜이런 식으로 기존 슬라이스와 새 슬라이스의 용량 변화를 관찰하고 규칙을 알아낼 수 있습니다. 🎜🎜실행 결과(🎜1.17 버전🎜): 🎜rrreee🎜실행 결과(🎜1.18 버전🎜): 🎜rrreee🎜 위 결과를 토대로 여전히 차이점을 확인할 수 있습니다. 구체적인 확장 전략은 소스를 보면서 설명하겠습니다. 암호. 🎜扩容调用的是 growslice
函数,我复制了其中计算新容量部分的代码。
// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... 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 } } } // ... return slice{p, old.len, newcap} }
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
// src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { // ... newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. 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 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } // ... return slice{p, old.len, newcap} }
和之前版本的区别,主要在扩容阈值,以及这行代码:newcap += (newcap + 3*threshold) / 4
。
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
newcap + 3*threshold
,直到新容量大于期望容量;分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。
那是为什么呢?
实际上,growslice
的后半部分还有更进一步的优化(内存对齐等),靠的是 roundupsize
函数,在计算完 newcap
值之后,还会有一个步骤计算最终的容量:
capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize)
这个函数的实现就不在这里深入了,先挖一个坑,以后再来补上。
切片扩容通常是在进行切片的 append
操作时触发的。在进行 append
操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice
函数进行扩容。
切片扩容分两个阶段,分为 go1.18 之前和之后:
一、go1.18 之前:
二、go1.18 之后:
newcap + 3*threshold
,直到新容量大于期望容量;以上就是本文的全部内容,如果觉得还不错的话欢迎点赞,转发和关注,感谢支持。
推荐学习:Golang教程
위 내용은 Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!