Go语言 设置存储片的容量有什么意义?

kse8i1jr  于 2022-12-31  发布在  Go
关注(0)|答案(4)|浏览(148)

在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()可以轻易地超过一个片的容量,那么最初定义它的意义何在?设置足够大的容量是否会带来性能上的提升?

s4n0splo

s4n0splo1#

切片实际上只是管理底层数组的一种奇特方式,它自动跟踪大小,并根据需要重新分配新的空间。
当您追加到切片时,运行时每次超出其当前容量时,其容量都会翻倍。为此,它必须复制所有元素。如果您在开始之前就知道它将有多大,则可以通过预先获取所有元素来避免一些复制操作和内存分配。
make某个片提供容量时,您设置的是初始容量,而不是任何类型的限制
有关切片的一些有趣的内部细节,请参阅这篇关于切片的博客文章。

np8igboo

np8igboo2#

slice是简单array的一个很好的抽象,你可以得到各种各样的好特性,但是在它的核心深处,存在着array。(我以相反的顺序解释下面的内容是有原因的)因此,如果/当您指定3capacity时,在内存中分配了一个长度为3的数组,您可以将append设置为最大,而无需重新分配内存。此属性在make命令中是可选的,但请注意,无论您是否选择指定capacityslice都将始终具有capacity。如果您指定length(它总是存在的),slice可以被索引到这个长度,capacity的其余部分隐藏在幕后,所以当使用append时,它不必分配一个全新的数组
下面是一个更好地解释机制的示例。
s := make([]int, 1, 3)
底层array将被分配int的零值3(即0):
[0,0,0]
但是,length被设置为1,因此切片本身将只打印[0],如果您尝试索引第二个或第三个值,它将打印panic,因为slice的机制不允许这样做。你会发现它实际上已经被创建为包含zero值直到length,并且你将以[0,1]结束。在整个底层array被填充之前,您可以再执行一次append,另一次append将强制它分配一个新的append,并以双倍的容量复制所有值。

**因此,**您问题的简短答案是,预分配capacity可用于极大地提高代码效率,尤其是当slice最终将变得非常大或包含复杂的structs时(或两者),因为structzero值实际上是它的fieldszero值。这不是因为它将避免分配那些值,因为它无论如何都必须这样做,而是因为X1 M39 N1 X每次需要调整底层数组的大小时必须重新分配充满这些零值的新X1 M40 N1 X。

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

tp5buhyn

tp5buhyn3#

正如其他人已经说过的,使用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%,所以这比1 m个元素的简单实现提高了大约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,则在某个时刻,程序将尝试访问不存在的插槽并死机。
j8ag8udp

j8ag8udp4#

每次向具有len(mySlice) == cap(mySlice)的切片添加项时,基础数据结构都会替换为更大的结构。

fmt.Printf("Original Capacity: %v", cap(mySlice))  // Output: 8
mySlice = append(mySlice, myNewItem)
fmt.Printf("New Capacity: %v", cap(mySlice))  // Output: 16

这里,mySlice被替换(通过赋值操作符)为一个新切片,该切片包含原始mySlice的所有元素,加上myNewItem,再加上一些空间(容量),以便在不触发调整大小的情况下进行增长。
可以想象,这个调整大小的操作在计算上非常重要。
通常情况下,如果**您知道需要在mySlice中存储多少项,就可以避免所有的调整大小操作,如果您有此预知,就可以预先设置原始切片的容量,从而避免所有的调整大小操作。
(In实践中,通常可以知道有多少项将被添加到集合中;尤其是在将数据从一种格式转换为另一种格式时。

相关问题