如何在 Go 中有效地从切片中删除元素?

How to efficiently delete an element from a slice in Go?

提问人:Evgeniy Mikhalev 提问时间:8/26/2023 最后编辑:Evgeniy Mikhalev 更新时间:8/26/2023 访问量:192

问:

有几种方法可以删除切片元素。但是,如果我有一个密集处理切片的应用程序怎么办。Go 切片在添加新元素方面进行了很好的优化,但是有没有一种有效的方法可以从切片中删除元素(不仅速度,而且内存优化)。

我知道切片。删除 Go 1.21 中引入的功能,但在后台,它使用了以下众所周知的技术:

return append(s[:i], s[j:]...)

在这种情况下,基础数组似乎不会减少。这对速度有好处,但是如果我们有很多元素(例如,100k 或 1M),然后将它们减少到很少(例如,只有 10 个),该怎么办?看起来没有像用于增加切片容量的内存优化。

当我们不需要保留切片中元素的顺序时,可以使用以下方法(go playground 链接):

func sliceDel[S ~[]E, E any](s S, i, j int) S {
    lastIdx := len(s) - (j - i)
    copy(s[i:], s[lastIdx:])
    return s[:lastIdx]
}

当我们有大切片和少量元素要删除时,这可能很有用(其背后的想法是复制少量切片元素)。

关于内存,在这两种情况下,容量将是相同的,并且不会减少。例如:

    // Reduce slice almost to zero
    for i := 0; i < sliceSize/2-1; i++ {
        sl = sliceDel(sl, 0, 2)
    }
    fmt.Printf("len = %d, cap = %d", len(sl), cap(sl))
        // Output: len = 2, cap = 100000

    // Reduce slice almost to zero
    for i := 0; i < sliceSize/2-1; i++ {
        sl = slices.Delete(sl, 0, 2)
    }
    fmt.Printf("len = %d, cap = %d", len(sl), cap(sl))
        // Output: len = 2, cap = 100000  

那么,有没有办法优化内存使用呢?例如,如果切片的长度小于其容量的一半,则将容量减少一半。

我也想知道如何有效地做到这一点,例如这样的技术(切片使用完整的切片表达式。Clip) 不会减少底层数组 - 它只会在切片结构中保存新容量,以避免在将新元素附加到子切片时重写父切片元素(如本提案中所述)。s[:len(s):len(s)]

Go 内存管理 切片

评论

0赞 JimB 8/26/2023
如果要确保使用较小的数组分配,则需要将元素复制到较小的数组中。从理论上讲,完整的切片表达式可能会在某个时候释放部分分配:请参阅 groups.google.com/g/golang-nuts/c/SNY2ZCiNV-U/m/oL2feeUlDAAJ

答:

1赞 icza 8/26/2023 #1

没有“一般最佳”的解决方案。您在问题中展示了多种方法,每种方法都可能比其他方法更好。

这对速度有好处,但是如果我们有很多元素(例如,100k 或 1M),然后将其减少到很少(例如,只有 10 个),该怎么办?

如果您遇到这种情况,当您想保留许多元素中的几个元素时,甚至不要开始删除这些元素。使用这几个元素构建一个新切片。除了速度更快之外,这肯定也解决了内存问题。

除了分配和使用新切片外,您不能通过使用完整切片表达式来减少内存使用量。只要有对后备数组的引用,它就不会缩小(至少在当前的 Go 版本中不会)。如果您处于分配了一个大的后备数组但只使用了其中的一小部分的情况,则可以分配一个新切片并手动复制元素,以便让大的切片进行垃圾回收。

还要考虑的是,如果您有一个大切片,您可能需要从中删除许多元素,那么切片可能不是最好的数据结构。例如,您可以尝试使用链表,甚至可以尝试使用地图:从链表或地图中删除元素会快得多,地图也会提供快速()查找时间。O(n)

评论

0赞 Evgeniy Mikhalev 8/26/2023
谢谢。我认为您提出的替代数据结构(哈希映射、链表)将是更好的解决方案。我只是想知道切片时是否有任何选择,可能是我错过了一些东西。
-2赞 Deltics 8/26/2023 #2

如果正如您所说(我没有理由怀疑),切片非常适合附加项目但在删除项目时效率不高的用例,并且您有一个需要从大量项目集合中执行大量有效删除的用例,那么您也许应该考虑使用切片以外的其他东西。

container/list 可能是一个候选者。