Classic interview question: Go slice expansion strategy

introduction

Recently, in the process of reviewing interview questions, because the local Go uses version 1.20, and the online description of the Go slice expansion strategy is still mostly in the version before 2021, that is, Go1.17 version and all previous versions, so share it.

If the Go version you are using is greater than or equal to Go1.18, you must pay attention.

< Go1.17:

  • new capacity calculation
    • If the expected size is more than 2 times the existing capacity, use the expected capacity directly
    • If the capacity is less than 1024 ( Go1.18后是256), expand the capacity by 2 times, otherwise expand the capacity by 1.25 times ( Go1.18后由表达式计算)
  • Final capacity calculation: In order to avoid memory fragmentation, it will be performed at the end  内存对齐计算, so the final result will be greater than or equal to the value calculated above.

> Go1.18:

  • The condition for 2 times expansion is adjusted from less than 1024 to小于256
  • The fixed expansion ratio of 1.25 times, 改成了根据增长因子(growth factor)扩容, and this growth factor will gradually become smaller as the slice capacity increases until it approaches 1.25 indefinitely. Compared with the direct transition from 2 times to 1.25 times, the introduction of the growth factor (2.0 -> 1.63 -> 1.44 -> 1.35 -> 1.30 -> ...) to make scaling smoother.
  • The memory alignment calculation rules remain unchanged

So, if you are asked in the interview and answer this detail, you should get extra points, right?

When will the expansion be triggered

Slice expansion occurs append()when . If the length of the underlying array of the slice is not enough to accommodate the newly added elements, the expansion will be triggered. At this time, the go compiler will call to growslice()determine the new capacity size, and then copy the old elements to the new underlying array.

Where is the source code

  • src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
 // ...
}

growslice()It is append()the corresponding underlying function call when adding elements.

Source code comparison

Go1.17 and previous versions:

// 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 and subsequent versions (the latest Go1.20 is consistent):

// 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 example

The so-called growth factor is actually a calculation formula:

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后由表达式计算,不再是固定值
  • 最终容量计算:为了避免内存碎片,最后会进行 内存对齐计算,所以最后的结果会大于等于上面计算的值。

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

Guess you like

Origin juejin.im/post/7233588291295346744