在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)
}
Run Code Online (Sandbox Code Playgroud)
我惊讶地看到这个程序打印出来:
[1 6 0 2 4 3 1]
这让我想知道 - 如果append()可以简单地超越它,最初定义切片容量的重点是什么?设置足够大的容量是否有性能提升?
cap*_*aig 17
切片实际上只是管理底层数组的一种奇特方式.它会自动跟踪大小,并根据需要重新分配新空间.
当您附加到切片时,每次超过其当前容量时,其容量会增加一倍.它必须复制所有元素才能做到这一点.如果你知道在开始之前会有多大,你可以通过预先抓取它来避免一些复制操作和内存分配.
当您make提供容量的切片时,您可以设置初始容量,而不是任何类型的限制.
Ray*_*ear 11
A slice是一个简单的精彩抽象array.你可以获得各种不错的功能,但在其核心深处,有一个很好的功能array.(由于某种原因,我以相反的顺序解释以下内容).因此,如果/当指定capacity的3,在内心深处,长度的阵列3中的存储器分配,其可以append高达无需它需要重新分配存储器.此属性在make命令中是可选的,但请注意,无论您是否选择指定一个属性,slice都将始终具有该属性capacity.如果指定a length(也始终存在),则slice可以索引到该长度.的其余部分capacity被隐藏在幕后所以它没有分配一个全新的阵列时append使用.
这是一个更好地解释机制的例子.
s := make([]int, 1, 3)
底层array将分配3零值int(即0):
[0,0,0]
但是,length设置为1,所以切片本身只会打印[0],如果你试图索引第二个或第三个值,它将会panic,因为它slice的机制不允许它.如果你s = append(s, 1)这样做,你会发现它实际上已被创建为包含zero最多的值length,你最终会得到[0,1].此时,您可以append在array填充整个底层之前再次使用,另一个append将强制它分配一个新的底层并使用双倍容量复制所有值.这实际上是一项相当昂贵的操作.
因此,对您的问题的简短回答是,预分配capacity可以用来极大地提高代码的效率.特别是如果slice要么最终会非常大,要么包含复杂structs(或两者),因为zeroa struct的zero值实际上是其中每一个的值fields.这不是因为它无论如何都会避免分配这些值,而是因为每次需要调整底层数组的大小时append都必须重新分配array这些零值的新值.
短操场示例:https://play.golang.org/p/LGAYVlw-jr
正如其他人已经说过的,使用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
}
Run Code Online (Sandbox Code Playgroud)
更好的方法 - 使用 cap 参数
func filterCap(input []float64) []float64 {
ret := make([]float64, 0, len(input))
for _, el := range input {
if el > .5 {
ret = append(ret, el)
}
}
return ret
}
Run Code Online (Sandbox Code Playgroud)
基准 (n=10)
filter 131 ns/op 56 B/op 3 allocs/op
filterCap 56 ns/op 80 B/op 1 allocs/op
Run Code Online (Sandbox Code Playgroud)
使用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
Run Code Online (Sandbox Code Playgroud)
由于对runtime.makeslice. 但是,更大的区别是内存分配(少 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
}
Run Code Online (Sandbox Code Playgroud)
不出所料,这个校准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]
}
Run Code Online (Sandbox Code Playgroud)
这比filterCap规模化快约 10% 。但是,除了更复杂之外,这种模式还不能提供与cap您尝试校准内存要求相同的安全性。
cap校准,如果您低估了所需的总容量,那么程序会在需要时自动分配更多容量。len所需的总数,程序就会失败。在这个例子中,如果你初始化为ret := make([]float64, len(input)/2),结果是len(output) > len(input)/2,那么在某些时候程序将尝试访问一个不存在的插槽并发生恐慌。