为什么string.Builder的Reset()方法不保留底层缓冲区?

7
// Reset resets the Builder to be empty.
func (b *Builder) Reset() {
    b.addr = nil
    b.buf = nil
}

这段代码片段来自于go strings.Builder源代码。缓冲区被设置为nil,而不是b.buf[:0]。为什么要将其设置为nil而不是保留其容量呢?
编辑:Reset()函数可以用于回收底层缓冲区并允许重新使用Builder结构体,但似乎初始化该结构体只需两个指针的边际成本很小,而底层数组可能要大得多,并且可以被重复使用。我觉得应该有一个Clear()函数,它可以保留底层缓冲区的容量,但将其长度减少到0,这实现起来非常简单。这让我相信有一个原因阻止了这样做,我很好奇是什么原因。

你对为什么应该保留这个限制有什么想法?在我看来,这似乎并不是真正的“重置为空”? - Martin Tournoij
我想我有点困惑于使用Reset与仅声明另一个Builder对象的重点和/或好处。 - ajoseps
好的,我认为没有区别;这只是一种更方便的重置方式。 - Martin Tournoij
@ajoseps 相同的字符串构建器实例可以在多个不同的角色之间共享:因此重置它有助于保持使用相同的实例,但只是清除它。 - zerkms
@zerkms 我看到它可以这样使用,但是string.Builder只是一个指针和缓冲区,本质上只是另一个指针。重置它似乎是一种边际成本,而不是创建另一个。如果底层缓冲区被保留,则每个actor都可以使用Builder而不必担心是否进行了额外的分配。 - ajoseps
3个回答

6

strings.Builder 的一种优化是在将 []byte 转换为 string 时,它不会复制字节。看一下它的 String() 方法:

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

这意味着重用缓冲区会破坏之前创建的字符串。并且这里有一个在playground上的证明:https://play.golang.org/p/gkSXRwi0-Ff

没错。Rob Pike在这里给出了相同的解释:https://github.com/golang/go/issues/24716#issuecomment-379098966 - oxley
但是bytes.BufferBytes()方法也提供了对底层存储的别名,并且它只是定义了结果切片在下一次缓冲区修改之前是有效的。为什么strings.Builder不是这样的呢? - undefined

0

Reset() 的目的是将 Builder 初始化为空状态(就像创建新的一样)。

这样做的好处是,与获取新的 Builder 相比,当程序的其他组件持有对现有 Builder 的引用并且您想要将其“重置”为初始状态而不刷新所有这些组件以获取新的引用时,可以实现该目的。


我认为我的主要障碍是缺乏一个“清除”缓冲区长度而不是重新初始化的函数。这是我的观点,但与允许底层缓冲区被重复使用的功能相比,重置功能似乎用处有限(到目前为止)。我觉得没有暴露那个功能的原因是什么?是安全问题还是其他原因?共享实例解释似乎只是一个次要的便利因素,而需要重新分配缓冲区的成本尤其是在它被用于大字符串时更高。 - ajoseps
1
这使问题有些变化,为什么没有 Clear()(或 Truncate())方法。strings.Builder 在 1.10 版本中被添加,以使内部切片不可变(并防止逃逸),这就是为什么它只能 Grow()Reset() 的原因。引用发布说明:“API 是 bytes.Buffer 的受限子集,允许它在 String 方法期间安全地避免复制数据的副本。” - blami
啊,有趣,我猜你可以用bytes.Buffer重新实现一个基本版本的Builder。防止转义不是我考虑过的事情,但我想知道为什么会这样。一旦Builder超出范围,底层缓冲区也将被GC回收,不是吗?还有什么东西会引用底层缓冲区以允许它逃逸? - ajoseps
当你执行(*Buffer).Bytes()时,会泄漏底层切片。我将用伪造的示例更新我的答案。 - blami
我刚刚意识到 StringBuilder 有一个函数可以防止逃逸分析的发生。不过我不确定它是如何工作的。func noescape(p unsafe.Pointer) unsafe.Pointer - ajoseps

0
如果 Reset 保留底层缓冲区,则长期存在的 Builder 将占用内存,以容纳它曾经构建的最长字符串。即使大部分未使用,为最长字符串分配的数组始终会存在。将缓冲区设置为 nil 可以让垃圾回收器回收这些潜在的大型缓冲区。

这仅适用于保持Builder不调用Reset()的情况。它将使Builder持久存在,直到超出范围为止。即使在分配更多空间后,您也无法真正重复使用它。我可以看到Reset()的实用性是返回要GC回收的空间,但缺少Clear()类型的函数会排除重新使用缓冲区的能力。它似乎强制Builder仅用于生成单个字符串,当它可以用于多个字符串时。 - ajoseps
1
如果在使用长期缓冲区调用 Reset 时,切片被截断了,那么它将占用比所需更大的空间,仅仅因为您偶然构建了一个大字符串。 - Burak Serdar
是的,那是真的,但似乎奇怪的是,截断选项甚至从一开始就不存在。调用者应该能够确定该空间是否有用。 - ajoseps
多年前,我曾参与过一次类似的讨论,但是主题是关于Java的。讨论的问题是,在构建字符串时,重用相同的缓冲区还是每次创建新的缓冲区更好。在实际负载下进行测试后,当时创建新的缓冲区表现更佳。 - Burak Serdar
那是怎么回事呢?你不会被重新分配的成本所击败吗,或者这是运行时能够优化的东西吗?例如,实际上并没有进行垃圾回收。 - ajoseps

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接