正如其他人已经说过的,使用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,那么在某些时候程序会尝试访问一个不存在的插槽并出现恐慌。