【问题标题】:Golang: appending slices with or w/o allocationGolang:附加或不分配分配的切片
【发布时间】:2016-02-20 09:05:41
【问题描述】:

Go 的 append() 函数仅在给定切片的容量不足时分配新的切片数据(另请参见:https://stackoverflow.com/a/28143457/802833)。这可能会导致意外行为(至少对于我作为 golang 新手而言):

package main

import (
    "fmt"
)

func main() {

    a1 := make([][]int, 3)
    a2 := make([][]int, 3)
    b := [][]int{{1, 1, 1}, {2, 2, 2}, {3, 3, 3}}
    common1 := make([]int, 0)
    common2 := make([]int, 0, 12) // provide sufficient capacity
    common1 = append(common1, []int{10, 20}...)
    common2 = append(common2, []int{10, 20}...)

    idx := 0
    for _, k := range b {
        a1[idx] = append(common1, k...) // new slice is allocated
        a2[idx] = append(common2, k...) // no allocation
        idx++
    }

    fmt.Println(a1)
    fmt.Println(a2) // surprise!!!
}

输出:

[[10 20 1 1 1] [10 20 2 2 2] [10 20 3 3 3]]

[[10 20 3 3 3] [10 20 3 3 3] [10 20 3 3 3]]

https://play.golang.org/p/8PEqFxAsMt

那么,Go 中强制分配新切片数据或更准确地说是确保 append() 的切片参数保持不变的(惯用)方式是什么?

【问题讨论】:

  • “问题”不是附加的行为,而是您的代码不必要的复杂。
  • @Volker 该示例是从更大的上下文中提取的。如果我只需要附加显示的切片,确实有一种更简单的方法:-)

标签: go append slice


【解决方案1】:

你可能对 Go 中切片的工作方式有一个错误的想法。

当您将元素附加到切片时,对append() 的调用会返回一个新切片。如果没有发生重新分配,两个切片值——你调用append()的那个和它返回的那个——共享相同的后备数组但是它们会有不同的长度;观察:

package main

import "fmt"

func main() {
    a := make([]int, 0, 10)
    b := append(a, 1, 2, 3)
    c := append(a, 4, 3, 2)
    fmt.Printf("a=%#v\nb=%#v\nc=%#v\n", a, b, c)
}

输出:

a=[]int{}
b=[]int{4, 3, 2}
c=[]int{4, 3, 2}

所以,len(a) == 0len(b) == 3len(c) == 3 以及对 append() 的第二次调用写出了第一次的作用,因为所有切片共享同一个底层数组。

关于后备数组的重新分配,the spec 很清楚:

如果 s 的容量不足以容纳附加值,则 append 分配一个新的、足够大的底层数组,该数组既适合现有的切片元素又适合附加值。否则,追加重用底层数组。

由此可知:

  1. append() 绝不会在附加到的切片容量足够的情况下复制底层存储。
  2. 如果容量不足,将重新分配数组。

也就是说,给定一个切片 s,您希望将 N 元素附加到该切片上,如果 cap(s) - len(s) ≥ N 则不会进行重新分配。

因此,我怀疑您的问题不在于意外的重新分配结果,而在于 Go 中实现的切片的概念。要吸收的代码思想是 append() 返回结果切片值, 你应该在调用之后使用它除非你完全理解后果。 p>

我建议从this 开始以完全理解它们。

【讨论】:

  • 感谢您指向博客文章。因此,为了实现所需的(和可重现的)行为,我可以写:b := make([]int, len(a), len(a)+3); copy(b, a); b = append(b, []int{1, 2, 3})
  • @kirk 如果你要附加到一个切片s,总是总是s = append(s, value),所以基本上是“是”。
  • @kirk,是的,为了确保附加的 N 个元素不会重新分配后备数组,创建一个容量足以容纳当前数据的切片 plus N,复制现有的切到新的,然后追加。
【解决方案2】:

感谢您的反馈。

因此,获得对内存分配的控制权的解决方案是明确地进行(这让我想起 Go 比其他(脚本)语言更像是一种系统语言):

package main

import (
    "fmt"
)

func main() {

    a1 := make([][]int, 3)
    a2 := make([][]int, 3)
    b := [][]int{{1, 1, 1}, {2, 2, 2}, {3, 3, 3}}
    common1 := make([]int, 0)
    common2 := make([]int, 0, 12) // provide sufficient capacity
    common1 = append(common1, []int{10, 20}...)
    common2 = append(common2, []int{10, 20}...)

    idx := 0
    for _, k := range b {
        a1[idx] = append(common1, k...) // new slice is allocated
        
        a2[idx] = make([]int, len(common2), len(common2)+len(k))
        copy(a2[idx], common2)      // copy & append could probably be
        a2[idx] = append(a2[idx], k...) // combined into a single copy step
        
        idx++
    }

    fmt.Println(a1)
    fmt.Println(a2)
}

输出:

[[10 20 1 1 1] [10 20 2 2 2] [10 20 3 3 3]]

[[10 20 1 1 1] [10 20 2 2 2] [10 20 3 3 3]]

https://play.golang.org/p/Id_wSZwb84

【讨论】:

  • 关于显式内存分配...是的,不是的。附加到切片的一个常见习惯用法是不打扰并执行s = append(s, e1, e2, ...) - 这样 Go 将确保在附加需要时重新分配支持数组。这使您无需考虑内存管理除非您有其他切片指向相同的数据(即您在附加到原始切片之前已对其进行切片)。但这是一个很好的权衡:它使简单案例变得简单,而复杂案例成为可能。
  • 关于我提到的那个习语的另一点是,有时你知道你的切片将有 至少 N 个元素(或可能更多)或 最多 N个元素。无论哪种情况,都可以进行微优化并通过s := make([]T, 0, N)分配长度为0、容量为N的切片。在前一种情况下,您将保证附加第一个 N 元素不会重新分配;在第二种情况下,您可能会浪费一些空间来换取附加速度。在任何一种情况下,请在分析之前不要优化!
猜你喜欢
  • 2016-07-07
  • 2016-12-03
  • 2018-04-15
  • 2018-06-06
  • 2016-05-25
  • 2019-01-02
  • 2016-01-15
  • 2023-03-09
相关资源
最近更新 更多