소스 코드 분석을 통해 Go 슬라이스 확장 메커니즘을 이해하고 대부분의 수문학에 오해하지 마세요.

1. 문제점 소개

1.1 원래 질문

Go 공식 구문 가이드Adding Elements to a Slice 섹션을 공부 하던 중 다음 데모를 실행했을 때 실행 결과의 마지막 줄을 cap = 6이해할 수 없다는 것을 발견했습니다. 이때 문제는 슬라이스에 3을 추가하는 것으로 설명할 수 있습니다. 한 번에 2개의 길이와 용량을 갖는 요소의 경우 최종 길이는 5인데 용량은 왜 6인가요?

package main

import "fmt"

func main() {
    
    
	var s []int
	printSlice(s)

	// 添加一个空切片
	s = append(s, 0)
	printSlice(s)

	// 这个切片会按需增长
	s = append(s, 1)
	printSlice(s)

	// 可以一次性添加多个元素
	s = append(s, 2, 3, 4)
	printSlice(s)
}

func printSlice(s []int) {
    
    
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

위 프로그램을 실행한 결과

len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]	//该行中的 cap=6 无法理解

Baidu에 대한 대부분의 연구를 거친 후 Go 언어 버전 1.18 에서 오해의 소지가 있는 슬라이스 확장 메커니즘이 다음과 같이 발견되었습니다 . ! ! 후속 분석을 통해 이 메커니즘이 불완전하다는 것을 알 수 있으므로 독자는 주의를 기울여야 합니다! ! ! ]

1. 슬라이스의 용량이 1024개 요소 미만인 경우 확장 시 슬라이스의 캡에 2를 곱하고, 요소 수가 1024개 요소를 초과하면 성장 인자는 1.25, 즉 원래의 1/4이 됩니다. 매번 용량이 늘어나네요..
2. 확장 후 원래 기본 배열의 용량을 초과하지 않은 경우 슬라이스의 포인터는 원래 배열을 가리킵니다. 확장 후 원래 기본 배열의 용량을 초과하면 Go는 다음을 엽니다. 새 메모리.먼저 원래 요소의 값을 복사한 다음 새 요소를 추가합니다. 이 상황은 원래 배열에 전혀 영향을 미치지 않습니다.

이 확장 메커니즘을 따르면 슬라이스의 기본 배열은 먼저 원래 용량의 2배(2 * 2 = 4)로 확장되어야 합니다. 여전히 모든 새 요소를 수용할 수 없는 것으로 확인된 후 계속해서 확장됩니다. 2번 확장(4*2=8) 즉, 마지막 줄은 cap8이 되어야 하는데, 현재로서는 문제가 아직 해결되지 않은 상태입니다.

1.2 새로운 질문

또한, 정보를 검토하는 과정에서 새로운 문제점이 발견되었는데, 다음 코드를 실행하면 확장 메커니즘 원리 1에 설명된 것과 다르며, 실행 결과는 100*2=200, 10000+10000*1.25=가 아니다. 11250이지만 22413312.

package main

import "fmt"

func main() {
    
    
	t := make([]int, 100)
	printSlice(t)

	t = append(t, 666)
	printSlice(t)
	
	fmt.Println("--------------------")
	
	r := make([]int, 10000)
	printSlice(r)
	
	r = append(r, 888)
	printSlice(r)

}

func printSlice(s []int) {
    
    
	fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
}

위 코드를 실행한 결과

len=100 cap=100
len=101 cap=224
--------------------
len=10000 cap=10000
len=10001 cap=13312

2. 소스코드 분석

2.1 성장슬라이스()

바이두에서 검색해서 정보를 찾아보니 %GOROOT%/src/runtime/slice.go해당 파일에 슬라이싱 확장을 담당하는 API가 존재하는 것을 발견했습니다.

// 位于 %GOROOT%/src/runtime/slice.go 文件中

func growslice(et *_type, old slice, cap int) slice {
    
    
    //省略部分判断代码
    
    //计算扩容部分
    //其中 cap : 所需容量(比如上述例子中 2 + 3 = 5),newcap : 最终申请的容量(最终打印的结果 6)
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
    
    
        newcap = cap
    } else {
    
    
        if old.len < 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
            }
        }
    }
 
    //省略部分判断代码
}

old.len < 1024위의 소스코드를 보면 , 비교에 앞서 cap(필요 용량)과 doublecap(원래 용량의 2배)의 관계를 먼저 비교한다는 것을 알 수 있습니다 . 이 예와 결합하여 원래 용량이 2인 슬라이스에 세 개의 새 요소를 추가합니다. 즉, cap = 2 + 3 = 5, doublecap = 2 + 2 = 4(doublecap = newcap + newcap = old.cap + old.cap), cap > doublecap그래서 최종 적용된 용량은 newcap = cap = 5. 이 시점에서 원래 질문은 마지막 줄의 상한선이 5가 아니라 6인 이유가 됩니다.

계속해서 goeslice() 함수를 살펴본 후 우리는 다음 코드를 발견했습니다. 여기서 스위치 분기 사례는 et.size유형 크기, 즉 유형이 차지하는 공간입니다. 이 예에서 조각에 있는 요소의 데이터 유형은 입니다 int. (int, uint 및 uintptr은 일반적으로 32비트 운영 체제에서 너비가 32비트 또는 4바이트이고, 64비트 운영 체제에서 너비가 64비트 또는 8바이트입니다.)

switch {
    
    
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == sys.PtrSize:
        lenmem = uintptr(old.len) * sys.PtrSize
        newlenmem = uintptr(cap) * sys.PtrSize
        capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
        newcap = int(capmem / sys.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if sys.PtrSize == 8 {
    
    
            // Mask shift for better code generation.
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
    
    
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }

여기서 capmem최종 요청된 용량의 메모리 크기는 이고 newcap유형은 입니다 uintptr.

// uintptr is an integer type that is large enough to hold the bit pattern of any pointer
// uintptr 是一个整数类型,不是指针类型,但是足够保存任何一种指针
type uintptr uintptr

오히려 sys.PtrSize포인터의 바이트 크기로, 32비트 운영 체제에서는 4바이트이고 64비트 운영 체제에서는 8바이트입니다.

// %GOROOT%/src/internal/goarch/goarch.go 文件中有 ptrSize 的定义和计算方式

// PtrSize is the size of a pointer in bytes - unsafe.Sizeof(uintptr(0)) but as an ideal constant.
// It is also the size of the machine's native word size (that is, 4 on 32-bit systems, 8 on 64-bit).
const PtrSize = 4 << (^uintptr(0) >> 63)

2.2 반올림 크기()

위의 코드를 보면 파일 에 있는 함수와 관련하여 capmem계산이 이루어지고 있음을 알 수 있으며 , 소스코드는 다음과 같습니다.roundupsize()%GOROOT%/src/runtime/msize.go

// 位于 %GOROOT%/src/runtime/msize.go 文件中

// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
    
    
    if size < _MaxSmallSize {
    
    
        if size <= smallSizeMax-8 {
    
    
            return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
        } else {
    
    
            return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])
        }
    }
    if size+_PageSize < size {
    
    
        return size
    }
    return round(size, _PageSize)
}

여기서 _MaxSmallSize의 크기는 32 << 10입니다 32768.
[ 파일에 정의됨] _MaxSmallSize = 32768[ 파일 정의됨 ]%GOROOT/src/runtime/sizeclasses.go
MaxSmallSize = 32 << 10%GOROOT/src/runtime/mksizeclasses.go

roundupsize()이 함수의 기능은 요청된 공간 크기에 따라 메모리 정렬을 고려한 후 실제 할당된 공간 크기를 반환하는 것 입니다 . 요청된 공간이 ,보다 작으면 , , _MaxSmallSize (32768)를 사용하여 인덱스를 계산하여 에서 값을 찾아 반환합니다.smallSizeDivsmallSizeMaxlargeSizeDivsize_to_classclass_to_size

2.3 원래 문제(1.1)의 실제 확장 과정

첫 번째 문제를 예로 들면, 이제 우리는 newcap = 5이고, 슬라이스 요소가 int유형이므로 et.size8바이트라는 것을 이미 알고 있습니다. 내 컴퓨터는 64비트 운영 체제이므로 sys.ptrSize역시 8바이트입니다. 따라서 먼저 의 명령문이 분기를 통과한 growslice()다음 , 즉 capmem = roundupsize(5 * 8), 5 * 8 = 40이 _MaxSmallSize-8보다 작으므로 실행됩니다. (size+smallSizeDiv-1) /smallSizeDiv] = ( 40+8-1)/8 = 5. 그런 다음 다음 배열을 확인하여 size_to_class8[5] = 5, class_to_size[5] = 48을 찾고 마지막으로 48을 반환합니다. 즉, 각 int 유형이 차지합니다 . 8바이트이므로 final은 첫 ​​번째 예제의 마지막 줄의 실행 결과를 성공적으로 설명합니다.switchcase et.size == sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])capmem = 48newcap = 48 / 8 = 6

//在 %GOROOT%/src/runtime/sizeclassed.go 文件中定义
const (
	_MaxSmallSize   = 32768
	smallSizeDiv    = 8
	smallSizeMax    = 1024
	largeSizeDiv    = 128
	_NumSizeClasses = 68
	_PageShift      = 13
)

var class_to_size = [_NumSizeClasses]uint16{
    
    0, 8, 16, 24, 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}

var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{
    
    0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}

var size_to_class128 = [(_MaxSmallSize-smallSizeMax)/largeSizeDiv + 1]uint8{
    
    32, 33, 34, 35, 36, 37, 37, 38, 38, 39, 39, 40, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 54, 54, 
......
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67}

3. 결론 요약

3.1 새로운 문제의 실제 확장 과정(1.2)

3.1.1 길이와 용량이 모두 100인 슬라이스에 요소 1개를 추가합니다. 최종 길이는 101인데 용량은 왜 224인가요?

이제 우리는 필요한 용량 cap = 101, 원래 용량의 2배 doublecap = 200, cap < doublecap, 새로 적용된 newcap = 200, 슬라이스 요소 유형이므로 8바이트, 내 컴퓨터는 64라는 것을 이미 알고 있습니다 int. et.size비트 운영 체제, sys.ptrSize역시 8바이트입니다. 따라서 먼저 의 명령문이 분기를 통과한 growslice()다음 , 즉 capmem = roundupsize(200 * 8), 200 * 8 = 1600이 _MaxSmallSize-8보다 작으므로 실행됩니다. (size+smallSizeDiv-1) /smallSizeDiv] = ( 1600+8-1)/8 = 200, 그런 다음 배열을 확인하여 size_to_class8[200] = 38, class_to_size[38] = 1792를 찾고 마지막으로 1792를 반환합니다. 즉, 각 int 유형이 차지합니다 . 8바이트이므로 결국 실행 결과를 성공적으로 해석했습니다.switchcase et.size == sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])capmem = 1792newcap = 1792 / 8 = 224

3.1.2 길이와 용량이 모두 10000인 슬라이스에 요소 1개를 추가합니다. 최종 길이는 10001인데 용량은 왜 13312인가요?

이제 우리는 필요한 용량 cap = 10001, 원래 용량의 2배 doublecap = 20000, cap < doublecap, 새로 적용된 newcap = 20000이라는 것을 이미 알고 있습니다. 슬라이스 요소는 유형이므로 8바이트입니다. 내 컴퓨터는 64 입니다 int. et.size비트 운영 체제, sys.ptrSize역시 8바이트입니다. 따라서 먼저 의 명령문이 분기를 통과한 growslice()다음 capmem = roundupsize(20000 * 8), 20000 * 8 = 160000이 _MaxSmallSize-8보다 크므로 실행됩니다. (size-smallSizeMax+largeSizeDiv- 1)/largeSizeDiv] ] = (160000-1024+128-1)/128 = 1242, 그런 다음 배열을 확인하여 size_to_class128[1242] = 168, class_to_size[168] = 106496을 찾고 마지막으로 106496을 반환합니다. 즉, capmem = 106496이고 각 int 유형이 8바이트를 차지하므로 결국 실행 결과가 성공적으로 해석되었습니다.switchcase et.size == sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])newcap = 106496 / 8 = 13312

3.2 이해하기 쉬운 확장 과정

위의 과정은 계산과 테이블 조회 작업이 포함된 프로그램 실행의 실제 과정입니다. 인간의 작업은 더 번거롭습니다. 또한 다음과 같은 이해 방법을 통해 최종 결과를 빠르게 얻을 수 있습니다. 첫 번째 문제를 예로 들어 보겠습니다. 이미 알고 있습니다. 필요한 용량 cap = 5, 원래 용량의 2배 doublecap = 4, cap < doublecap, 새로 적용된 newcap = 5; 슬라이스 요소는 유형이므로 8바이트입니다. 내 컴퓨터는 64비트 int입니다 et.size. 운영 체제, sys.ptrSize또한 8바이트입니다. 그래서 먼저 의 문이 분기를 거치게 되고 growslice(), 그 다음에는 capmem = roundupsize(5 * 8) 즉, 전달된 매개변수가 40이 됩니다 . 파일에는 다음과 같은 코드가 있습니다. 우리는 오직 세 번째 열은 간격으로 이해됩니다. 예를 들어 전달된 매개 변수가 특정 간격에 있으면 간격의 상한이 반환됩니다. 매개변수 40을 전달하고 40이 (32,48] 범위에 있으면 결국 48이 반환됩니다. 즉, capmem = roundupsize(5 * 8) = 48이고 각 int 유형은 8바이트를 차지하므로 the end , 첫 번째 예의 마지막 줄의 결과를 빠르고 성공적으로 설명합니다.switchcase et.size == sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)roundupsize()%GOROOT%/src/runtime/sizeclasses.gobytes/span(8,16](48,64]roundupsize()newcap = 48 / 8 = 6

3.1.1과 3.1.2의 문제점도 이 방법을 통해 이해할 수 있으며, 다음으로 3.1.1의 이해 과정을 설명하겠다. 질문이 있으시면 댓글 영역에 메시지를 남겨주세요~.

3.1.1의 경우 이제 필요한 용량 cap = 101, 원래 용량의 2배 doublecap = 200, cap < doublecap, newcap에 새로 적용된 = 200, 슬라이스 요소 유형이므로 8이라는 것을 이미 알고 있습니다 int. et.size단어 섹션; 내 컴퓨터는 64비트 운영 체제이며 sys.ptrSize역시 8바이트입니다. 따라서 먼저 의 문이 분기를 통과한 growslice()다음 , 즉 capmem = roundupsize(200 * 8), 즉 전달된 매개 변수는 1600이고 1600은 범위 내에 있으며 결국 1792가 반환됩니다. is, capmem = 1792, 각 int 유형은 8바이트를 차지하므로 결과를 최종적으로 빠르고 성공적으로 해석합니다.switchcase et.size == sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)roundupsize()(1536,1792]newcap = 1792 / 8 = 224

// 位于 %GOROOT%/src/runtime/sizeclasses.go 文件中

// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8
//     4         32        8192      256           0     21.88%         32
//     5         48        8192      170          32     31.52%         16
//     6         64        8192      128           0     23.44%         64
//     7         80        8192      102          32     19.07%         16
//     8         96        8192       85          32     15.95%         32
//     9        112        8192       73          16     13.56%         16
//    10        128        8192       64           0     11.72%        128
//    11        144        8192       56         128     11.82%         16
//    12        160        8192       51          32      9.73%         32
//    13        176        8192       46          96      9.59%         16
//    14        192        8192       42         128      9.25%         64
//    15        208        8192       39          80      8.12%         16
//    16        224        8192       36         128      8.15%         32
//    17        240        8192       34          32      6.62%         16
//    18        256        8192       32           0      5.86%        256
//    19        288        8192       28         128     12.16%         32
//    20        320        8192       25         192     11.80%         64
//    21        352        8192       23          96      9.88%         32
//    22        384        8192       21         128      9.51%        128
//    23        416        8192       19         288     10.71%         32
//    24        448        8192       18         128      8.37%         64
//    25        480        8192       17          32      6.82%         32
//    26        512        8192       16           0      6.05%        512
//    27        576        8192       14         128     12.33%         64
//    28        640        8192       12         512     15.48%        128
//    29        704        8192       11         448     13.93%         64
//    30        768        8192       10         512     13.94%        256
//    31        896        8192        9         128     15.52%        128
//    32       1024        8192        8           0     12.40%       1024
//    33       1152        8192        7         128     12.41%        128
//    34       1280        8192        6         512     15.55%        256
//    35       1408       16384       11         896     14.00%        128
//    36       1536        8192        5         512     14.00%        512
//    37       1792       16384        9         256     15.57%        256
//    38       2048        8192        4           0     12.45%       2048
//    39       2304       16384        7         256     12.46%        256
//    40       2688        8192        3         128     15.59%        128
//    41       3072       24576        8           0     12.47%       1024
//    42       3200       16384        5         384      6.22%        128
//    43       3456       24576        7         384      8.83%        128
//    44       4096        8192        2           0     15.60%       4096
//    45       4864       24576        5         256     16.65%        256
//    46       5376       16384        3         256     10.92%        256
//    47       6144       24576        4           0     12.48%       2048
//    48       6528       32768        5         128      6.23%        128
//    49       6784       40960        6         256      4.36%        128
//    50       6912       49152        7         768      3.37%        256
//    51       8192        8192        1           0     15.61%       8192
//    52       9472       57344        6         512     14.28%        256
//    53       9728       49152        5         512      3.64%        512
//    54      10240       40960        4           0      4.99%       2048
//    55      10880       32768        3         128      6.24%        128
//    56      12288       24576        2           0     11.45%       4096
//    57      13568       40960        3         256      9.99%        256
//    58      14336       57344        4           0      5.35%       2048
//    59      16384       16384        1           0     12.49%       8192
//    60      18432       73728        4           0     11.11%       2048
//    61      19072       57344        3         128      3.57%        128
//    62      20480       40960        2           0      6.87%       4096
//    63      21760       65536        3         256      6.25%        256
//    64      24576       24576        1           0     11.45%       8192
//    65      27264       81920        3         128     10.00%        128
//    66      28672       57344        2           0      4.91%       4096
//    67      32768       32768        1           0     12.50%       8192

4. 참고문헌

  1. 어학여행을 가다
  2. Go 언어 슬라이싱, 초기화, 확장, 용량 제한 및 하위 레이어에 대한 자세한 설명
  3. Golang 슬라이스 확장 메커니즘 소스 코드 분석
  4. Go의 슬라이스 확장은 모두 1.25 확장을 기반으로 한 것이 아니고 메모리 정렬이라는 개념도 있으니 다시는 속지 마세요.
  5. Go 슬라이스의 사용법과 본질

Supongo que te gusta

Origin blog.csdn.net/Alan_Walker688/article/details/127765704
Recomendado
Clasificación