기사 디렉토리
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) 즉, 마지막 줄은 cap
8이 되어야 하는데, 현재로서는 문제가 아직 해결되지 않은 상태입니다.
1.2 새로운 질문
또한, 정보를 검토하는 과정에서 새로운 문제점이 발견되었는데, 다음 코드를 실행하면 확장 메커니즘 원리 1에 설명된 것과 다르며, 실행 결과는 100*2=200, 10000+10000*1.25=가 아니다. 11250이지만 224
및 13312
.
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)
를 사용하여 인덱스를 계산하여 및 에서 값을 찾아 반환합니다.smallSizeDiv
smallSizeMax
largeSizeDiv
size_to_class
class_to_size
2.3 원래 문제(1.1)의 실제 확장 과정
첫 번째 문제를 예로 들면, 이제 우리는 newcap = 5이고, 슬라이스 요소가 int
유형이므로 et.size
8바이트라는 것을 이미 알고 있습니다. 내 컴퓨터는 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은 첫 번째 예제의 마지막 줄의 실행 결과를 성공적으로 설명합니다.switch
case et.size == sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
capmem = 48
newcap = 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바이트이므로 결국 실행 결과를 성공적으로 해석했습니다.switch
case et.size == sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
capmem = 1792
newcap = 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바이트를 차지하므로 결국 실행 결과가 성공적으로 해석되었습니다.switch
case et.size == sys.PtrSize
capmem = 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 , 첫 번째 예의 마지막 줄의 결과를 빠르고 성공적으로 설명합니다.switch
case et.size == sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
roundupsize()
%GOROOT%/src/runtime/sizeclasses.go
bytes/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바이트를 차지하므로 결과를 최종적으로 빠르고 성공적으로 해석합니다.switch
case et.size == sys.PtrSize
capmem = 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