切片扩容大法

Golang
462
0
0
2022-06-25

Q:你先简单介绍一下自己吧

A:嘛哩嘛哩哄,嘛哩嘛哩哄,嘛哩嘛哩哄

Q:你说这么多嘛哩嘛哩哄,那你了解数组吗?

A:数组是具有相同类型且长度固定的数据结构,由于长度不可改变,在实际中使用较少,一般用切片来代替使用。

Q:你刚刚说到切片,切片和数组有什么相同点和不同点吗?

A:相同点是他们都是只能储存相同类型的数据结构,切片的底层数据也是基于数组的,不同点是切片是可以改变长度的,能自动扩容。

Q:嗯,那切片的底层是怎么组成的呢,它的扩容规则你清楚吗?

A:切片由三部分组成,分别是切片的长度,切片的容量,切片的数据(指向底层数组的指针),至于扩容规则可就难说了。

Q:难说也是要说的,你搞快点。

A:切片扩容的基本规则是:当切片append的时候(会调用下面的函数),新切片的必须容量大于老切片容量的两倍的,那么新切片的容量就是新切片的必须容量,反之,如果老切片的长度小于1024,那么新切片的容量就等于老切片的容量的两倍,如果老切片大于等于1024,则新切片容量等于老切片容量的1.25倍,这是切片的预估容量。

// runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
//et 表示slice的一个元素,old 表示旧的slice cap 表示新切片必须的容量
    ...
    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
            }
        }
    }
    ...
}

根据以上规则 ,一下切片扩容必须的切片容量为 5 ,5 > 2*2,所以打印出来应该是len=5, cap=5

package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2}
    s = append(s, 4, 5, 6)
    fmt.Printf("len=%d, cap=%d", len(s), cap(s))
}

但是实际打印却是 len=5, cap=6,原来容量计算完了后还要考虑到内存的高效利用,进行内存对齐,则会调用这个函数

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 alignUp(size, _PageSize)
}

size 表示新切片需要的内存大小 我们传入的int 类型,每个占用8字节(可以调用unsafe.Sizeof()函数查看占用的大小),一共5个 所以是40,size小于_MaxSmallSize 并且小于 smallSizeMax-8 ,那么使用通用公式(size+smallSizeDiv-1)/smallSizeDiv 计算得到 5,

然后到size_to_class8 找到第五号元素 为 4,再从 class_to_size 找到 第四号元素 为 48,这就是新切片占用的内存大小,每个int 占用 8 字节,所以最终切片的容量为 6 。所以说切片的扩容有它基本的扩容规则,在规则之后还要考虑内存对齐,这就代表不同数据类型的切片扩容的容量大小是会不一致。

Q:好,那我们进入下一个问题