设置切片容量有什么意义?

时间:2017-07-31 19:16:40

标签: go slice

在Golang中,我们可以使用内置make()函数来创建具有给定初始长度和容量的切片。

考虑以下几行,切片的长度设置为1,其容量为3:

func main() {
    var slice = make([]int, 1, 3)
    slice[0] = 1
    slice = append(slice, 6, 0, 2, 4, 3, 1)
    fmt.Println(slice)
}

我很惊讶地看到这个程序打印出来:

  

[1 6 0 2 4 3 1]

这让我想知道 - 如果append()可以简单地超越它,那么最初定义切片容量的重点是什么?设置足够大的容量是否会带来性能提升?

3 个答案:

答案 0 :(得分:14)

切片实际上只是管理底层数组的一种奇特方式。它会自动跟踪大小,并根据需要重新分配新空间。

当您附加到切片时,每次超过其当前容量时,其容量会增加一倍。它必须复制所有元素才能做到这一点。如果你知道在开始之前会有多大,你可以通过预先获取它来避免一些复制操作和内存分配。

当您make切片提供容量时,您设置初始容量,而不是任何限制

有关切片的一些有趣的内部细节,请参阅this blog post on slices

答案 1 :(得分:7)

slice是简单array的精彩抽象。你可以获得各种不错的功能,但在其核心深处,有一个array。 (由于某种原因,我以相反的顺序解释以下内容)。因此,如果/当您指定capacity 3时,在内存中会分配一个长度为3的数组,您可以append直到需要重新分配内存。此属性在make命令中是可选的,但请注意,无论您是否选择指定slicecapacity始终都会有length。如果您指定slice(也始终存在),则capacity可以索引到该长度。 append的其余部分隐藏在幕后,因此在使用s := make([]int, 1, 3)时不必分配一个全新的数组

这是一个更好地解释机制的例子。

array

基础3将使用int的零值0进行分配([0,0,0]):

length

但是,1设置为[0],因此切片本身只会打印panic,如果您尝试索引第二个或第三个值,它将{{1}因为slice的机制不允许它。如果您s = append(s, 1),则会发现它实际上已创建为包含zero之前的length值,您最终会得到[0,1]。此时,您可以在填充整个基础append之前再次array,而另一个append将强制它分配一个新的,并以增加的容量复制所有值。这实际上是一项相当昂贵的操作。

因此对您的问题的简短回答是预先分配capacity可以大大提高代码的效率。特别是如果slice最终会非常大,或者包含复杂的structs(或两者),因为zero的{​​{1}}值实际上是{ {1}} struct中每一个的值{1}}。这不是因为它无论如何都会避免分配这些值,但是因为zero每次需要调整基础数组的大小时都必须重新分配新的fields以及这些零值。

短操场示例:https://play.golang.org/p/LGAYVlw-jr

答案 2 :(得分:1)

正如其他人已经说过的那样,使用cap参数可以避免不必要的分配。为了了解性能差异,假设您有一个[]float64随机值,并且想要一个新的切片来过滤掉不高于0.5的值。

天真的方法-没有len或cap参数

func filter(input []float64) []float64 {
    ret := make([]float64, 0)
    for _, el := range input {
        if el > .5 {
            ret = append(ret, el)
        }
    }
    return ret
}

更好的方法-使用上限参数

func filterCap(input []float64) []float64 {
    ret := make([]float64, 0, len(input))
    for _, el := range input {
        if el > .5 {
            ret = append(ret, el)
        }
    }
    return ret
}

基准(n = 10)

filter     131 ns/op    56 B/op  3 allocs/op
filterCap   56 ns/op    80 B/op  1 allocs/op

使用cap使程序的运行速度提高2倍以上,并将分配的数量从3减少到1。现在大规模发生了什么?

基准(n = 1,000,000)

filter     9630341 ns/op    23004421 B/op    37 allocs/op
filterCap  6906778 ns/op     8003584 B/op     1 allocs/op

由于对runtime.makeslice的调用减少了36个,因此速度差异仍然很大(〜1.4倍)。但是,最大的不同是内存分配(减少了约4倍)。

更好-校准瓶盖

您可能已经在第一个基准测试中注意到cap使整体内存分配更糟(80B vs 56B)。这是因为您分配了10个插槽,但平均仅需要5个插槽。这就是为什么您不想将cap设置得过高的原因。根据您对程序的了解,您可以校准容量。在这种情况下,我们可以估算出,经过过滤的切片将需要比原始切片多50%的时隙。

func filterCalibratedCap(input []float64) []float64 {
    ret := make([]float64, 0, len(input)/2)
    for _, el := range input {
        if el > .5 {
            ret = append(ret, el)
        }
    }
    return ret
}

毫不意外的是,经过校准的cap分配的内存是其前任产品的50%,因此,在1m元素的天真的实现上,性能提高了约8倍。

另一种选择-使用直接访问权限而不是追加

如果您希望在这样的程序上节省更多时间,请使用len参数进行初始化(并忽略cap参数),直接访问新切片而不是使用append,然后丢弃所有您不需要的广告位。

func filterLen(input []float64) []float64 {
    ret := make([]float64, len(input))
    var counter int
    for _, el := range input {
        if el > .5 {
            ret[counter] = el
            counter++
        }
    }
    return ret[:counter]
}

在规模上,这比filterCap快10%。但是,除了更复杂之外,如果您尝试校准内存需求,此模式将无法提供与cap相同的安全性。

  • 使用cap校准时,如果您低估了所需的总容量,则该程序将在需要时自动分配更多容量。
  • 使用这种方法,如果您低估了所需的总数len,该程序将失败。在此示例中,如果您初始化为ret := make([]float64, len(input)/2),而事实证明是len(output) > len(input)/2,则程序将在某个时候尝试访问不存在的插槽和出现紧急情况。