Поймите механизм расширения срезов Go на основе анализа исходного кода, не вводитесь в заблуждение большинством гидрологий.

1. Введение проблемы

1.1 Исходный вопрос

Когда я запустил следующую демонстрацию во время изучения раздела «Добавление элементов в срез» в официальном руководстве по синтаксису Go, я обнаружил, что последняя строка текущего результата непонятна.На данный момент проблему можно описать как добавление 3 к срезу. при длине и емкости сразу 2 элемента, конечная длина 5, но почему емкость 6?cap = 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, вводящий в заблуждение механизм расширения срезов в версии 1.18 языка Go был обнаружен следующим образом: [! ! ! После последующего анализа видно, что этот механизм несовершенен, читателям следует обратить внимание! ! !

1. Если емкость среза меньше 1024 элементов, то шапка среза при расширении умножается на 2, как только количество элементов превышает 1024 элемента, коэффициент роста становится 1,25, то есть четверть от исходного. емкость увеличивается каждый раз.
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 растет срез()

После поиска в Baidu и поиска информации я обнаружил, что в %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перед сравнением сначала сравнивается соотношение между ограничением (требуемая мощность) и двойным ограничением (2-кратная исходная мощность). В сочетании с этим примером добавьте три новых элемента в срез с исходной емкостью 2 с помощью метода Append(), а именно: cap = 2 + 3 = 5, doublecap = 2 + 2 = 4 (doublecap = newcap + newcap = old.cap + старый. колпачок), cap > doublecapпоэтому окончательная прикладная мощность newcap = cap = 5. На этом этапе возникает первоначальный вопрос: почему ограничение в последней строке не равно 5, а равно 6?

Продолжив рассматривать функцию Groslice(), мы обнаружили следующий код, где в случае ветки переключателя et.sizeуказан размер типа, то есть пространство, занимаемое типом. Тип данных элементов среза в этом примере — int. (int, uint и uintptr обычно имеют ширину 32 бита или 4 байта в 32-битных операционных системах; ширину 64 бита или 8 байтов в 64-битных операционных системах.)

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это размер указателя в байтах, который составляет 4 байта в 32-битной операционной системе и 8 байтов в 64-битной операционной системе.

// %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 байт. Итак, сначала оператор in growslice()пройдет switchчерез case et.size == sys.PtrSizeветку, затем capmem = roundupsize(uintptr(newcap) * sys.PtrSize), то есть capmem = roundupsize(5 * 8), 5 * 8 = 40 меньше, чем _MaxSmallSize-8, поэтому он будет выполнен return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])(size+smallSizeDiv-1) /smallSizeDiv] = ( 40+8-1)/8 = 5. Затем проверьте следующий массив и найдите, что size_to_class8[5] = 5, class_to_size[5] = 48, и, наконец, верните 48, то есть каждый тип int capmem = 48занимает 8 байт, поэтому Final 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 Добавьте 1 элемент в срез, длина и емкость которого равны 100. Конечная длина равна 101, но почему емкость равна 224?

Теперь мы уже знаем, что требуемое ограничение емкости = 101, в 2 раза больше исходной емкости, doublecap = 200, cap < doublecap, затем вновь примененное значение newcap = 200; элемент среза имеет тип, поэтому его размер составляет 8 байт; мой компьютер int- et.size64 битная операционная система, sys.ptrSizeтоже 8 байт. Итак, сначала оператор in growslice()пройдет switchчерез case et.size == sys.PtrSizeветку, затем capmem = roundupsize(uintptr(newcap) * sys.PtrSize), то есть capmem = roundupsize(200 * 8), 200 * 8 = 1600 меньше, чем _MaxSmallSize-8, поэтому он будет выполнен return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])(size+smallSizeDiv-1) /smallSizeDiv] = ( 1600+8-1)/8 = 200, а затем проверьте массив и найдите, что size_to_class8[200] = 38, class_to_size[38] = 1792, и, наконец, верните 1792, то есть каждый тип int capmem = 1792занимает 8 байт, так что в итоге newcap = 1792 / 8 = 224результаты выполнения успешно интерпретированы.

3.1.2 Добавьте 1 элемент в срез, длина и емкость которого равны 10 000. Конечная длина равна 10 001, но почему емкость равна 13 312?

Теперь мы уже знаем, что требуемое ограничение емкости = 10001, 2-кратное исходное значение емкости doublecap = 20000, cap < doublecap, затем вновь примененное значение newcap = 20000; элемент среза имеет тип, поэтому он имеет размер 8 байт; мой компьютер int- et.size64 битная операционная система, sys.ptrSizeтоже 8 байт. Итак, сначала оператор in growslice()пройдет switchпо case et.size == sys.PtrSizeветке, затем capmem = roundupsize(uintptr(newcap) * sys.PtrSize), то есть capmem = roundupsize(20000 * 8), 20000 * 8 = 160000 больше, чем _MaxSmallSize-8, поэтому он будет выполнен return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])(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 байт, поэтому в итоге newcap = 106496 / 8 = 13312результат выполнения был успешно интерпретирован.

3.2 Понятный процесс расширения

Вышеупомянутый процесс представляет собой фактический процесс выполнения программы, который включает в себя вычисления и операции поиска в таблицах. Операции, выполняемые человеком, более сложны. Мы также можем быстро получить окончательный результат с помощью следующих методов понимания: Взяв в качестве примера первую возникшую проблему, теперь мы уже известно Требуемый предел емкости = 5, в 2 раза больше исходной емкости doublecap = 4, cap < doublecap, затем вновь примененный newcap = 5; элемент среза имеет тип, поэтому он имеет размер 8 байт; мой компьютер 64 int- et.sizeбитный операционная система, sys.ptrSizeтоже 8 байт. Итак, сначала оператор in growslice()пройдет switchпо case et.size == sys.PtrSizeветке, затем capmem = roundupsize(uintptr(newcap) * sys.PtrSize), то есть capmem = roundupsize(5 * 8), то есть roundupsize()передаваемый параметр равен 40. %GOROOT%/src/runtime/sizeclasses.goВ файле есть следующий код. Мы ориентируемся только на третий столбец, bytes/spanпод которым можно понимать интервалы, например (8,16], (48,64]если переданный параметр находится в определенном интервале, будет возвращена верхняя граница интервала. Когда мы roundupsize()передаем параметр 40, а 40 находится в диапазоне (32,48), в конечном итоге будет возвращено 48, то есть capmem = roundupsize(5 * 8) = 48, и каждый тип int занимает 8 байт, поэтому в конец newcap = 48 / 8 = 6. Быстро и успешно объясняет результаты последней строки первого примера.

Проблемы в 3.1.1 и 3.1.2 также можно понять с помощью этого метода.Далее я опишу процесс понимания 3.1.1.Для 3.1.2 читателям предлагается вывести его самостоятельно.Если вы Если у вас есть какие-либо вопросы, вы можете оставить сообщение в области комментариев~.

Для версии 3.1.1 мы уже знаем, что требуемое ограничение емкости = 101, 2-кратное исходное значение емкости doublecap = 200, cap < doublecap, затем вновь примененное значение newcap = 200; элемент среза имеет тип, поэтому он равен int8 et.sizeРаздел слов; На моем компьютере установлена ​​64-битная операционная система, которая sys.ptrSizeтакже имеет размер 8 байт. Итак, сначала оператор in growslice()пройдет switchчерез case et.size == sys.PtrSizeветку, затем capmem = roundupsize(uintptr(newcap) * sys.PtrSize), то есть capmem = roundupsize(200 * 8), то есть roundupsize()переданный параметр равен 1600, 1600 находится в (1536,1792]диапазоне, и в конечном итоге будет возвращено 1792, что есть, capmem = 1792, и каждый тип int занимает 8 байт, поэтому окончательная 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

Guess you like

Origin blog.csdn.net/Alan_Walker688/article/details/127765704