经典面试题:Go Slice切片扩容策略

该面试题已收录整理到 interview-golang 开源项目中,点击查看更多Go后端相关面试题,持续更新中……

引言

最近在刷面试题的过程中,因为本地Go使用的是1.20版本,而网上关于 Go slice扩容策略的描述还大多停留在 2021年前的版本,也就是Go1.17版本和之前的所有版本,遂分享出来。

如果你使用的Go版本大于等于Go1.18,可要千万注意啦。

Go1.18之前:

  • 新容量计算
    • 如果期望大小超过现有容量2倍,则直接使用期望容量
    • 如果容量小于1024(Go1.18后是256),2倍扩容,否则1.25倍扩容(Go1.18后由表达式计算
  • 最终容量计算:为了避免内存碎片,最后会进行 内存对齐计算,所以最后的结果会大于等于上面计算的值。

Go1.18开始:

  • 2倍扩容的条件由小于1024调整为小于256
  • 1.25倍的固定扩容比,改成了根据增长因子(growth factor)扩容,而这个增长因子会随着切片容量越大而逐渐变小,直到无限趋近于1.25,相比从2倍直接过渡到1.25倍,增长因子的引入(2.0 -> 1.63 -> 1.44 -> 1.35 -> 1.30 -> …)让扩容更加平滑。
  • 内存对齐计算规则保持不变

so,如果面试被问到,回答了这个细节,应该会加分吧?

什么时候会触发扩容

切片扩容发生在调用 append() 时,如果切片的底层数组长度已经不足以容纳新添加的元素时,就会触发扩容,此时go编译器会调用 growslice() 确定新的容量大小,然后拷贝老的元素到新的底层数组。

扩容源码在哪里

主要看Go源码的2个文件(Go1.17版本):

  • src/cmd/compile/internal/ssagen/ssa.go
func (s *state) append(n *ir.CallExpr, inplace bool) *ssa.Value{
 // ...
}
  • src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
 // ...
}

growslice() 是使用append()添加元素时对应的底层函数调用, 而 ssa.go 文件中,我们可以知道编译器是如何给 runtime 传参的,比如参数cap实际上是切片的len+3。

源码对比

Go1.17和之前的版本

// go.17 src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {// cap=len+3,只有当切片长度<=2时走这个逻辑,故可忽略
        newcap = cap
    } else {
        // 1. 先计算新的容量大小
        if old.cap < 1024 {
            newcap = doublecap
        } else {
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    // … 内存对齐计算、数据拷贝等逻辑
}

Go1.18和后续版本(最新的Go1.20保持一致):

// 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 {
            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
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    //...后续代码(内存对齐等)没有变动
}

增长因子(growth factor)示例

所谓的增长因子其实就是一个计算公式:

newcap += (newcap + 3*threshold) / 4

如果看不懂,Keith Randall 大神在2021年9月的提交中给出了一个增长因子的示例(runtime: make slice growth formula a bit smoother):

runtime: make slice growth formula a bit smoother

    Instead of growing 2x for < 1024 elements and 1.25x for >= 1024 elements,
    use a somewhat smoother formula for the growth factor. Start reducing
    the growth factor after 256 elements, but slowly.

    starting cap    growth factor
    256             2.0
    512             1.63
    1024            1.44
    2048            1.35
    4096            1.30

通过这个示例,我们只要明白大切片(超过256)扩容时,不再是固定 1.25 倍,而是平滑下降即可。

分析公式,当 newcap无限大时,作为分子的 3threshold(3256)因为是固定值,故可以忽略不计,所以增长因子会无限趋近于 1.25倍大小。

总结

扩容策略:

  • 新容量计算
    • 如果期望大小超过现有容量2倍,则直接使用期望容量
    • 如果容量小于1024(Go1.18后是256),2倍扩容,否则1.25倍扩容(Go1.18后由表达式计算,不再是固定值
  • 最终容量计算:为了避免内存碎片,最后会进行 内存对齐计算,所以最后的结果会大于等于上面计算的值。

面试题已收录整理到 interview-golang 开源项目中,点击查看更多Go后端相关面试题。


作者简介:一线Gopher,公众号《Go和分布式IM》运营者,开源项目: CoffeeChatinterview-golang 发起人 & 核心开发者,终身程序员。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Golang中,切片扩容是通过内置函数append来实现的。具体实现方法是使用slice结合golang内置方法append进行动态扩容。\[1\]当切片的容量不足以容纳新的元素时,append函数会创建一个新的底层数组,并将原来的元素复制到新的数组中。然后,将新的元素添加到新的数组中,并返回一个新的切片。这样就实现了切片扩容切片的底层也是在连续的内存块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。\[2\]切片是一个非常小的对象,它是对底层的数组进行了抽象,并且提供了相关的操作方法。它拥有三个字段,分别是指向底层数组的指针、长度和容量。通过对切片再次切片,可以缩小一个切片的大小。\[3\]所以,通过使用append函数和切片的特性,可以实现切片的动态扩容。 #### 引用[.reference_title] - *1* *3* [golang slice扩容机制](https://blog.csdn.net/qq_52696089/article/details/126171790)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Golang Slice切片如何扩容](https://blog.csdn.net/moer0/article/details/122933748)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值