在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()
可以简单地超越它,那么最初定义切片容量的重点是什么?设置足够大的容量是否会带来性能提升?
答案 0 :(得分:14)
切片实际上只是管理底层数组的一种奇特方式。它会自动跟踪大小,并根据需要重新分配新空间。
当您附加到切片时,每次超过其当前容量时,其容量会增加一倍。它必须复制所有元素才能做到这一点。如果你知道在开始之前会有多大,你可以通过预先获取它来避免一些复制操作和内存分配。
当您make
切片提供容量时,您设置初始容量,而不是任何限制。
有关切片的一些有趣的内部细节,请参阅this blog post on slices。
答案 1 :(得分:7)
slice
是简单array
的精彩抽象。你可以获得各种不错的功能,但在其核心深处,有一个array
。 (由于某种原因,我以相反的顺序解释以下内容)。因此,如果/当您指定capacity
3
时,在内存中会分配一个长度为3
的数组,您可以append
直到需要重新分配内存。此属性在make
命令中是可选的,但请注意,无论您是否选择指定slice
,capacity
始终都会有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
以及这些零值。
答案 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
,则程序将在某个时候尝试访问不存在的插槽和出现紧急情况。