Go中使用append进行前置的机制是什么



假设我有一个类型为int的切片slice。在声明时,我将第三个参数设置为size,我认为通过设置切片的cap参数,它至少为sizeint保留了内存。

slice:=make([]int,0,size)

现在,假设我有一个整数变量value。为了将其添加到最后的切片中,我使用

slice=append(slice,value)

如果片中当前的元素数量小于size,则不需要将整个底层阵列复制到新位置来添加新元素。

此外,如果我想将value预处理为slice,如这里和这里所建议的,我使用

slice=append([]int{value},slice...)

我的问题是,在这种情况下会发生什么?如果元素的数量仍然小于size,那么这些元素是如何存储在存储器中的?假设调用make()函数时有连续分配,是否所有现有元素都被右移以释放第一个值空间?还是重新分配内存并复制所有元素?

问这个问题的原因是,我希望我的程序尽可能快,并想知道这是否是放慢速度的可能原因。如果是这样的话,有没有其他更节省时间的备考方式?

带有重新切片和复制

内置的append()总是元素附加到切片中。不能(单独)使用它来为元素添加前缀。

话虽如此,如果你有一个大于长度的切片容量(在其元素后面有"空闲"空间),你想为其准备一个元素,你可以重新切片原始切片,将所有元素复制到一个更高的索引,为新元素腾出空间,然后将元素设置为第0个索引。这将不需要新的分配。这就是它的样子:

func prepend(dest []int, value int) []int {
if cap(dest) > len(dest) {
dest = dest[:len(dest)+1]
copy(dest[1:], dest)
dest[0] = value
return dest
}
// No room, new slice need to be allocated:
// Use some extra space for future:
res := make([]int, len(dest)+1, len(dest)+5)
res[0] = value
copy(res[1:], dest)
return res
}

测试:

s := make([]int, 0, 5)
s = append(s, 1, 2, 3, 4)
fmt.Println(s)
s = prepend(s, 9)
fmt.Println(s)
s = prepend(s, 8)
fmt.Println(s)

输出(在Go Playground上尝试):

[1 2 3 4]
[9 1 2 3 4]
[8 9 1 2 3 4]

注意:如果没有空间容纳新元素,由于性能现在很重要,我们不只是这样做:

res := append([]int{value}, dest...)

因为它执行的分配和复制比需要的多:为文字([]int{value})分配一个切片,然后append()在向其添加dest时分配一个新的。

相反,我们的解决方案只分配一个新阵列(通过make(),甚至为未来的增长保留一些空间),然后只将value设置为第一个元素并复制dest(以前的元素)。

带链接列表

如果你需要多次准备,一个普通的切片可能不是正确的选择。一个更快的替代方案是使用链表,在链表之前准备一个元素不需要分配切片/数组和复制,只需创建一个新的节点元素,并通过将其指向旧的根(第一个元素)将其指定为根。

标准库在container/list包中提供了一个通用实现。

通过手动管理更大的备份阵列

坚持使用普通切片和数组,还有另一种解决方案。

如果您愿意自己管理更大的备份阵列(或切片),可以在使用的切片之前留出可用空间。在进行预处理时,可以从支持较大的数组或切片创建一个新的切片值,该值从为1个要进行预处理的元素留出空间的索引开始。

没有完整性,仅用于演示:

var backing = make([]int, 15) // 15 elements
var start int
func prepend(dest []int, value int) []int {
if start == 0 {
// No more room for new value, must allocate bigger backing array:
newbacking := make([]int, len(backing)+5)
start = 5
copy(newbacking[5:], backing)
backing = newbacking
}
start--
dest = backing[start : start+len(dest)+1]
dest[0] = value
return dest
}

测试/使用:

start = 5
s := backing[start:start] // empty slice, starting at idx=5
s = append(s, 1, 2, 3, 4)
fmt.Println(s)
s = prepend(s, 9)
fmt.Println(s)
s = prepend(s, 8)
fmt.Println(s)
// Prepend more to test reallocation:
for i := 10; i < 15; i++ {
s = prepend(s, i)
}
fmt.Println(s)

输出(在Go Playground上尝试):

[1 2 3 4]
[9 1 2 3 4]
[8 9 1 2 3 4]
[14 13 12 11 10 8 9 1 2 3 4]

分析:当backing切片中有空间准备值时,此解决方案不进行分配和复制所发生的一切是,它从backing切片创建一个新的切片,该切片覆盖要预处理的值的目标+1空间,设置它并返回该切片值。你真的没有比这更好的了。

如果没有空间,那么它会分配一个更大的backing切片,复制旧的内容,然后进行"正常"的预处理。

使用棘手的切片

想法:想象一下,你总是以向后的顺序将元素存储在一个切片中。

在切片中按向后顺序存储元素意味着预包装变成了附加

因此,要"预封装"一个元素,只需使用append(s, value)即可。仅此而已。

是的,这有其有限的用途(例如,以相反顺序存储的切片的附加与"正常"切片和预加载操作具有相同的问题和复杂性),并且您失去了许多便利性(仅凭名称即可使用for range列出元素),但就性能而言,没有什么比仅使用append()预加载值更好的了。

注意:在按向后顺序存储元素的元素上迭代必须使用向下循环,例如:

for i := len(s) - 1; i >= 0; i-- {
// do something with s[i]
}

最后一点:所有这些解决方案都可以很容易地扩展到预处理切片,而不仅仅是一个值。通常,重新排序时的附加空间不是+1,而是+len(values),并且不是简单地设置dst[0] = value,而是对copy(dst, values)的调用。

"prepend"调用将需要分配一个数组并复制所有元素,因为Go中的切片被定义为起点、大小和分配(从起点开始计算分配)。

切片不可能知道第一个元素之前的元素可以用于扩展切片。

会发生什么

slice = append([]int{value}, slice...)

  1. 为单个元素value分配一个新数组(可能在堆栈上)
  2. 创建一个切片来映射此元素(start=0,size=1,alloc=1)
  3. append调用完成
  4. append发现没有足够的空间来扩展单个元素切片,因此分配一个新的数组并复制所有元素
  5. 将创建一个新的切片对象来引用此数组

如果在大型容器的两端添加/删除是应用程序的常见用例,那么您需要一个deque容器。不幸的是,它在Go中不可用,并且不可能在保持可用性的同时高效地编写包含泛型的类型(因为Go仍然缺乏泛型)。

然而,您可以为您的特定情况实现deque,这很容易(例如,如果您有一个具有已知上限的大型容器,则可能只需要一个循环缓冲区,而这只需要几行代码)。


我对围棋很陌生,所以下面可能是非常糟糕的围棋代码。。。但这是一种尝试,使用一个不断增长的循环缓冲区来实现deque(根据使用情况,这可能是一个好的解决方案,也可能不是)

type Deque struct {
buffer  []interface{}
f, b, n int
}
func (d *Deque) resize() {
new_buffer := make([]interface{}, 2*(1+d.n))
j := d.f
for i := 0; i < d.n; i++ {
new_buffer[i] = d.buffer[j]
d.buffer[j] = nil
j++
if j == len(d.buffer) {
j = 0
}
}
d.f = 0
d.b = d.n
d.buffer = new_buffer
}
func (d *Deque) push_back(x interface{}) {
if d.n == len(d.buffer) {
d.resize()
}
d.buffer[d.b] = x
d.b++
if d.b == len(d.buffer) {
d.b = 0
}
d.n++
}
func (d *Deque) push_front(x interface{}) {
if d.n == len(d.buffer) {
d.resize()
}
if d.f == 0 {
d.f = len(d.buffer)
}
d.f--
d.buffer[d.f] = x
d.n++
}
func (d *Deque) pop_back() interface{} {
if d.n == 0 {
panic("Cannot pop from an empty deque")
}
if d.b == 0 {
d.b = len(d.buffer)
}
d.b--
x := d.buffer[d.b]
d.buffer[d.b] = nil
d.n--
return x
}
func (d *Deque) pop_front() interface{} {
if d.n == 0 {
panic("Cannot pop from an empty deque")
}
x := d.buffer[d.f]
d.buffer[d.f] = nil
d.f++
if d.f == len(d.buffer) {
d.f = 0
}
d.n--
return x
}

相关内容

  • 没有找到相关文章

最新更新