【问题标题】:Is reframing a slice in Go a constant-time operation?在 Go 中重构切片是一个恒定时间操作吗?
【发布时间】:2020-11-19 09:02:12
【问题描述】:

我在 Go 中有一个坐标对 ([x, y]) 的 FIFO 队列:

type Copair [2]int
type Queue []Copair
var q = Queue{ ... preloaded values ... }

API 定义如下,其中重要的部分是pop() 函数:

func (q Queue) push(p Copair) {
    q = append(q, p)
}

func (q Queue) pop() (error, Copair) {
    if q != nil && len(q) >= 1 {
        q = q[1:]
        return nil, q[0]
    } else {
        return errors.New("index out of range [0]"), nil
    }
}

q = q[1:] 中,我正在重新构建切片,理论上它应该只需要在内存中更改一个地址,因此是一个恒定时间操作。现在,我们正在逐渐丢失堆上的字节(或者谁知道,Go 的逃逸分析可能足够聪明,可以将它放在堆栈上),我希望垃圾收集器可以回收这些字节以避免内存泄漏,最终我们将到达堆边界,运行时将不得不将整个队列复制到其他地方,这肯定是线性时间操作。但是……

q = q[1:] 执行的切片重构是恒定时间操作,还是与队列大小成线性关系?如果(正如我所怀疑的)答案是“哦,非常”“它取决于”,它依赖的条件是什么,是否有任何简单的经验法则可以解决这个问题?

【问题讨论】:

  • “应该只需要在内存中更改一个地址”它需要更改一个值,但它不是地址,而是索引。 “我希望垃圾收集器可以回收这些字节”不,它不能;切片是对底层数组的引用,该数组的大小没有变化,只有切片覆盖的窗口。
  • 我可以要求一些建设性的反馈,说明为什么这被否决了吗?这似乎是一个完全合理的问题,如果我犯了一些大错误,我希望能够在未来改进我的问题:)

标签: go memory time-complexity slice


【解决方案1】:

切片是一个常数时间的操作。切片头包含指向底层数组、大小和容量的指针。操作q:=q[1:] 只是创建一个带有调整后的数组指针、大小和容量的新标头。

【讨论】:

    【解决方案2】:

    q[1:] 是一个slice expression,它“无非”只是创建一个新的切片头,即reflect.SliceHeader。它只是 3 个整数值。

    当然会检查索引范围,例如如果q 切片为空,则会导致运行时恐慌。

    【讨论】:

    • 嗯,这改变了一些事情。那么底层内存呢? GC 在什么时候看到切片中删除的索引不再有对它们的引用?
    • 切片是指后备数组的连续部分。只要有一个切片头指向后备数组的某些部分,整个后备数组就会保存在内存中。当不再引用它时,它就有资格进行垃圾回收。
    • 那么因为在这种情况下切片头正在更新但底层数组只是在增长,这只是一个相当优雅的内存泄漏?
    • @TheEnvironmentalist 不,这不是泄漏。弹出将更改切片标题以排除数组的开头,但推送元素将追加到末尾。一旦后备数组不足以追加,append() 分配一个新的后备数组,并复制内容,只有被切片覆盖的部分,开始,被遗弃的部分不会被复制。在这样的复制之后,旧的后备数组可以被垃圾收集。因此,如果您继续使用队列、推送、弹出,新分配自然会释放未使用的开始部分。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-04
    • 1970-01-01
    • 2014-09-21
    • 2016-07-22
    • 2018-06-17
    • 2018-12-05
    • 1970-01-01
    相关资源
    最近更新 更多