在类似StringBuilder的C模块中增加多少缓冲区?

2022-09-02 11:34:26

在C中,我正在研究一个管理字节缓冲区的“类”,允许将任意数据追加到末尾。我现在正在研究自动调整大小,因为底层数组使用对的调用填满。对于任何曾经使用过Java或C#的人来说,这应该是有意义的。我了解如何调整大小。但是,有没有人有任何建议,并提供理由,关于每次调整缓冲区增加多少reallocStringBuilder

显然,在浪费的空间和过多的 realloc 调用(这可能导致过度复制)之间需要进行权衡。我看到一些教程/文章建议加倍。如果用户设法提供一个好的初始猜测,这似乎是浪费。是否值得尝试在平台上舍入到对齐大小的两倍或倍数?

有没有人知道Java或C#在引擎盖下做了什么?


答案 1

在 C# 中,用于增大 StringBuilder 使用的内部缓冲区的策略随着时间的推移而变化。

有三种基本策略可以解决此问题,并且它们具有不同的性能特征。

第一个基本策略是:

  • 创建字符数组
  • 当您的空间不足时,为一些常量 k 创建一个包含 k 更多字符的新数组。
  • 将旧数组复制到新数组,并孤立旧数组。

这种策略存在许多问题,其中最明显的是,如果正在构建的字符串非常大,那么它在时间上是O(n2)。假设 k 是一千个字符,最后一个字符串是一百万个字符。您最终将字符串重新分配到 1000、2000、3000、4000、...因此复制1000 + 2000 + 3000 + 4000 + ... + 999000个字符,这总共复制了大约5000亿个字符!

此策略具有很好的特性,即“浪费”的内存量以 k 为限。

在实践中,由于n平方问题,这种策略很少使用。

第二个基本策略是

  • 创建数组
  • 当您的空间不足时,为一些常量 k 创建一个字符数增加 k% 的新数组。
  • 将旧数组复制到新数组,并孤立旧数组。

k%通常为100%;如果是这样,那么这被称为“满时加倍”策略。

这种策略具有很好的属性,即其摊销成本为O(n)。再次假设最后一个字符串是一百万个字符,你从一千个字符开始。您在1000,2000,4000,8000,...最终复制了1000 + 2000 + 4000 + 8000 ... + 512000个字符,这相当于复制了大约一百万个字符;好多了。

该策略具有这样的属性:无论您选择什么百分比,摊销成本都是线性的。

此策略有许多缺点,即有时复制操作非常昂贵,并且您可能会在未使用的内存中浪费高达最终字符串长度的 k%

第三种策略是制作数组的链接列表,每个数组的大小为k。当现有数组溢出时,将分配一个新数组并将其追加到列表的末尾。

此策略具有很好的特性,即没有操作是特别昂贵的,总浪费的内存受 k 限制,并且您不需要能够定期在堆中找到大块。它的缺点是,最终将事物转换为字符串可能会很昂贵,因为链接列表中的数组可能具有较差的位置。

.NET 框架中的字符串生成器曾经使用双满策略;它现在使用块的链接列表策略。


答案 2

您通常希望保持生长因子略小于黄金均值(~1.6)。当它小于黄金均值时,丢弃的线段将足够大,可以满足以后的请求,只要它们彼此相邻即可。如果你的生长因子大于黄金平均值,那就不可能发生。

我发现将因子减少到1.5仍然很好,并且具有易于在整数数学中实现的优点(--使用一个像样的编译器,您可以将其编写为,并且它仍然应该编译为快速代码)。size = (size + (size << 1))>>1;(size * 3)/2

我似乎记得几年前在Usenet上的一次谈话,其中Dinkumware的P.J. Plauger(或者可能是Pete Becker)说他们运行了比我更广泛的测试,并得出了同样的结论(例如,在他们的C++标准库中实现使用1.5)。std::vector


推荐