> 백엔드 개발 > Golang > Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석

Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석

青灯夜游
풀어 주다: 2023-04-19 19:21:48
앞으로
916명이 탐색했습니다.

Go 언어 슬라이싱은 어떻게 확장되나요? 다음 글에서는 Go 언어의 슬라이스 확장 메커니즘을 소개하겠습니다. 도움이 되길 바랍니다.

Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석

Go 언어에는 매우 일반적으로 사용되는 데이터 구조, 즉 슬라이스가 있습니다.

슬라이스는 동일한 유형의 요소로 구성된 가변 길이 시퀀스입니다. 배열 유형을 기반으로 하는 캡슐화 계층입니다. 매우 유연하며 자동 확장을 지원합니다.

슬라이스(Slice)는 Pointer, LengthCapacity라는 세 가지 속성을 갖는 참조 유형입니다.

Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석

기본 소스 코드는 다음과 같이 정의됩니다.

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
로그인 후 복사
  1. 포인터: 슬라이스가 액세스할 수 있는 첫 번째 요소를 가리킵니다.
  2. 길이: 슬라이스의 요소 수입니다.
  3. 용량: 슬라이스의 시작 요소와 기본 배열의 마지막 요소 사이의 요소 수입니다.

예를 들어 make([]byte, 5)를 사용하여 다음과 같은 슬라이스를 생성합니다. make([]byte, 5) 创建一个切片,它看起来是这样的:

Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석

声明和初始化

切片的使用还是比较简单的,这里举一个例子,直接看代码吧。

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

Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석

선언 및 초기화

슬라이싱의 사용법은 비교적 간단합니다. 코드를 직접 살펴보겠습니다.

[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)
로그인 후 복사
로그인 후 복사
🎜출력 결과입니다. is: 🎜rrreee🎜 🎜expansion🎜의 예를 살펴보겠습니다. 🎜rrreee🎜출력 결과는 다음과 같습니다. 🎜rrreee🎜 이 예에서는 길이가 0이고 용량이 3 조각. 그런 다음 append 함수를 사용하여 5 요소를 슬라이스에 추가합니다. 🎜🎜 4 요소를 추가하면 슬라이스 길이가 용량을 초과합니다. 이때 슬라이스가 자동으로 확장됩니다. 새로운 용량은 원래 용량인 6의 두 배입니다. 🎜🎜피상적인 현상을 살펴보았습니다. 다음으로 슬라이싱 확장 메커니즘이 어떤 모습인지 알아보기 위해 소스 코드 수준으로 자세히 살펴보겠습니다. 🎜

🎜소스 코드 분석🎜

🎜Go 언어의 소스 코드에서는 일반적으로 append 작업을 수행할 때 슬라이스 확장이 트리거됩니다. 일부분. append 작업 중에 슬라이스 용량이 새 요소를 수용하기에 충분하지 않으면 슬라이스를 확장해야 합니다. 이때 growslice 함수가 호출되어 확장됩니다. 용량. 🎜🎜growslice 함수는 Go 언어의 런타임 패키지에 정의되어 있으며 해당 호출은 컴파일된 코드에서 구현됩니다. 구체적으로 append 작업이 수행되면 컴파일러는 이를 다음과 유사한 코드로 변환합니다. 🎜rrreee🎜위 코드에서 슬라이스 용량이 새 요소를 수용할 만큼 충분하지 않으면 확장을 위해 growslice 함수가 호출됩니다. 따라서 growslice 함수에 대한 호출은 소스 코드에서 명시적으로 호출되는 것이 아니라 생성된 기계어 코드에서 컴파일러에 의해 구현됩니다. 🎜🎜슬라이스 확장 전략에는 go1.18 이전과 이후가 다른 두 단계가 있습니다. 이에 대해서는 go1.18 릴리스 노트에 설명되어 있습니다. 🎜🎜아래에서는 go1.17과 go1.18 버전을 사용하여 별도로 설명하겠습니다. 먼저, 두 버전 간의 확장 차이를 직관적으로 느껴보기 위해 테스트 코드를 살펴보겠습니다. 🎜rrreee🎜위 코드는 먼저 빈 슬라이스를 만든 다음 루프에서 새 요소를 해당 슬라이스에 추가합니다. 🎜🎜그런 다음 용량이 변경될 때마다 기존 용량과 추가된 요소, 요소를 추가한 후의 용량을 기록합니다. 🎜🎜이런 식으로 기존 슬라이스와 새 슬라이스의 용량 변화를 관찰하고 규칙을 알아낼 수 있습니다. 🎜🎜실행 결과(🎜1.17 버전🎜): 🎜rrreee🎜실행 결과(🎜1.18 버전🎜): 🎜rrreee🎜 위 결과를 토대로 여전히 차이점을 확인할 수 있습니다. 구체적인 확장 전략은 소스를 보면서 설명하겠습니다. 암호. 🎜

go1.17

扩容调用的是 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}
}
로그인 후 복사
로그인 후 복사

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

go1.18

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

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
  3. 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;

内存对齐

分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。

那是为什么呢?

实际上,growslice 的后半部分还有更进一步的优化(内存对齐等),靠的是 roundupsize 函数,在计算完 newcap 值之后,还会有一个步骤计算最终的容量:

capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
로그인 후 복사
로그인 후 복사

这个函数的实现就不在这里深入了,先挖一个坑,以后再来补上。

总结

切片扩容通常是在进行切片的 append 操作时触发的。在进行 append 操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice 函数进行扩容。

切片扩容分两个阶段,分为 go1.18 之前和之后:

一、go1.18 之前:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

二、go1.18 之后:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
  3. 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;

以上就是本文的全部内容,如果觉得还不错的话欢迎点赞转发关注,感谢支持。

推荐学习:Golang教程

위 내용은 Go 언어 슬라이싱이 어떻게 확장되는지에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿