【问题标题】:Buffer growth strategy缓冲增长策略
【发布时间】:2010-02-15 21:29:39
【问题描述】:

我有一个通用的增长缓冲区,旨在累积“随机”字符串片段,然后获取结果。 Code 处理该缓冲区是用纯 C 编写的。

伪代码 API:

void write(buffer_t * buf, const unsigned char * bytes, size_t len);/* appends */
const unsigned char * buffer(buffer_t * buf);/* returns accumulated data */

我正在考虑我应该为该缓冲区选择的增长策略。

我不知道我的用户是否更喜欢内存或速度——或者用户数据的性质是什么。

我在野外看到了两种策略:以固定大小增量增长缓冲区(这是我目前实现的)或以指数方式增长数据。 (还有一种策略可以分配所需的确切内存量——但这在我的例子中并不那么有趣。)

也许我应该让用户选择策略...但这会使代码更复杂...

曾几何时,Herb Sutter wrote(引用 Andrew Koenig)认为最好的策略可能是指数增长,系数为 1.5(搜索“增长策略”)。这仍然是最好的选择吗?

有什么建议吗?你的经历说明了什么?

【问题讨论】:

  • 嘿,这个问题有一些我在 StackOverflow 上收到的最酷的回答帖子。谢谢大家的答案!我已经接受了我发现最有趣的答案——但这是一个艰难的选择。其余的答案也很好。谢谢!

标签: c buffer


【解决方案1】:

除非您有充分的理由不这样做,否则指数增长可能是最佳选择。使用 1.5 作为指数并不是很神奇,事实上这不是 Andrew Koenig 最初所说的。他原来说的是生长因子应该小于(1+sqrt(5))/2(~1.6)。

Pete Becker 说,当他在 Dinkumware 时 Dinkumware 的所有者 P.J. Plauger 说,他们做了一些测试,发现 1.5 运行良好。当您分配一块内存时,分配器通常会分配一个至少比您要求的稍大的块,以便为它留出空间来存放一些簿记信息。我的猜测(尽管未经任何测试证实)是稍微减小该因子可以让实际块大小仍然在限制范围内。

参考资料: 我相信 Andrew 最初在一本杂志(面向对象编程杂志,IIRC)上发表了这篇文章,该杂志已经多年未出版,因此重新印刷可能会非常困难。

Andrew Koenig's Usenet postP.J. Plauger's Usenet post

【讨论】:

  • 很酷的细节,谢谢!您是否有任何参考资料?我很想阅读它们。
【解决方案2】:

使用指数增长(无论因子是 1.5 还是 2)的要点是避免复制。每次重新分配数组时,都可以触发项目的隐式副本,当然,它越大,成本越高。通过使用指数增长,您可以获得摊销的常数复制次数 - 即您很少最终复制。

只要您在某种台式计算机上运行,​​您就可以期待基本上无限量的内存,因此时间可能是这种权衡的正确选择。对于硬实时系统,您可能希望找到一种完全避免复制的方法——想到一个链表。

【讨论】:

    【解决方案3】:

    我通常使用一个小的固定数量的加法和乘以 1.5 的组合,因为它实现起来很有效,并导致合理的步宽,起初更大,当缓冲区增长时对内存更敏感。作为固定偏移量,我通常使用缓冲区的初始大小并从相当小的初始大小开始:

    new_size = old_size + ( old_size >> 1 ) + initial_size;
    

    作为 initial_size,我将 4 用于集合类型,8、12 或 16 用于字符串类型,128 到 4096 用于输入/输出缓冲区,具体取决于上下文。

    这是一个小图表,它显示与仅乘以 1.5(红色)相比,它在早期步骤中增长得更快(黄色+红色)。

    因此,如果您从 100 开始,则需要增加 6 个以容纳 3000 个元素,而仅乘以 1.5 则需要 9 个。

    在较大的尺寸下,加法的影响可以忽略不计,这使得两种方法的缩放倍数一样好,为 1.5 倍。如果您使用初始大小作为添加的固定量,这些是有效的增​​长因素:

    2.5
    1.9
    1.7
    1.62
    1.57
    1.54
    1.53
    1.52
    1.51
    1.5
    ...
    

    【讨论】:

      【解决方案4】:

      在整个 STL 中都使用指数增长策略,而且它似乎运行良好。我会说至少坚持下去,直到你找到它不起作用的明确案例。

      【讨论】:

      • 除了std::vector还有什么用?
      • std::vector(和 std::string)也是我的主要示例,但它们几乎是 STL 中唯一的连续内存类。
      • 啊,std::string 是有道理的,但它不在 STL 中。 (如果您指的是 C++ 标准库,请使用 stdlib。)我也在想同样的事情,所有其他容器都是不连续的。
      • 字符串 可能 是不连续的,但是 c_str() 和 data() 的实现会很糟糕。
      • @GMan:STL 是一个特定的“容器类、算法和迭代器”库 (sgi.com/tech/stl/stl_introduction.html),它从未包含 basic_string 或 iostreams。这并不意味着 stdlib 中的所有模板,并且名称会令人困惑。另见hpl.hp.com/techreports/95/HPL-95-11.html
      【解决方案5】:

      关键是指数增长策略可以让您在达到当前大小的成本时避免昂贵的副本缓冲区内容>一些浪费的内存。您链接的文章有用于交易的数字。

      【讨论】:

        【解决方案6】:

        答案一如既往,“取决于”。

        指数增长背后的想法 - 即分配一个 x 倍于当前大小的新缓冲区是,当您需要更多缓冲区时,您将需要更多缓冲区,并且您可能需要比小固定增量提供。

        所以,如果你有一个 8 字节的缓冲区,并且需要更多分配一个额外的 8 个字节是可以的,那么分配一个额外的 16 个字节可能是一个好主意 - 拥有 16 字节缓冲区的人可能不需要额外的 1 个字节。如果他们这样做了,所发生的一切就是你在浪费一点记忆。

        我认为最好的增长因子是 2 - 即双倍缓冲,但如果 Koenig/Sutter 说 1.5 是最佳的,那么我同意他们的观点。不过,您可能希望在获得一些使用统计数据后调整您的增长率。​​p>

        因此,指数增长是性能和保持低内存使用率之间的良好权衡。

        【讨论】:

        • 这真的取决于应用程序的性质。例如,每次将缓冲区加倍会使内存管理系统更容易保持地址空间不碎片化。对于长时间运行的应用程序,保持空间不碎片化(以避免将内存浪费在太小而无法使用的块中)可能比使用 1.5 而不是 2 节省的额外内存更重要。跨度>
        【解决方案7】:
        • 将大小翻倍直到达到阈值 (~100MB?),然后将指数增长降低到 1.5,.., 1.3
        • 另一个选项是在运行时配置默认缓冲区大小。

        【讨论】:

        • 这对于尚未证明是问题的事情来说太复杂了——更不用说是问题了。这种复杂性很难被检测为错误的原因。
        【解决方案8】:

        如果不了解分配、运行时环境、执行特性等方面的知识,任何人都无法给出好的建议。

        有效的代码比高度优化的代码更重要......正在开发中。选择一些算法——任何可行的算法——并尝试一下!如果它被证明是次优的,那么就改变策略。将其置于图书馆用户的控制之下通常对他们没有好处。但是如果你已经有了一些选项方案,那么添加它可能会很有用,除非你找到了一个好的算法(n^1.5 是一个非常好的算法)。


        此外,在 C(非 C++)中使用名为 write 的函数与 冲突。如果没有使用它们很好,但以后添加它们也很困难。最好使用更具描述性的名称。

        【讨论】:

        • <io.h> 是非常特定于 MSVC 的...在 Unix 中,write<unistd.h> 中声明。 :-P 但是,是的,不管你是否包含<unistd.h> 之类的,事实是write 是在libc 中定义的,并且自己实现write,尤其是与系统不兼容的那个,是不明智。
        • 这就是为什么我说这是伪代码。 :-) 实际函数称为lbsSB_write()。无论如何感谢您的警告。
        • @Chris: <io.h> 是 Unix 的旧标准头文件,其中包含 read()write()seek() 等。许多嵌入式开发人员运行时库延续了这一传统。
        • 我很难在我正在查看的任何旧版本的 Unix 中找到这个头文件(谷歌在搜索 "io.h" Unix 时也没有任何运气):我的版本我正在查看来自 Unix Heritage Society 的 minnie.tuhs.org/Archive/PDP-11/Trees。请注意,在 V6 和 V7 中都没有io.h;系统调用是直接进行的。 2.11BSD 树的特征是unistd.h
        【解决方案9】:

        作为一个疯狂的想法,对于这种特定情况,您可以更改 API 以要求 调用者 为每个块分配内存,然后记住这些块而不是复制数据。 p>

        然后,到了实际产生结果的时候,您就可以准确地知道需要多少内存并且可以准确地分配这些内存。

        这样做的好处是调用者无论如何都需要为块分配内存,因此您不妨利用它。这也避免了多次复制数据。

        它的缺点是调用者必须动态分配每个块。为了解决这个问题,您可以为每个块分配内存并记住它们,而不是保留一个大缓冲区,当它满时会调整大小。这样,您将复制数据两次(一次复制到您分配的块中,另一次复制到结果字符串中),但不再复制。如果您必须多次调整大小,最终可能会得到两个以上的副本。

        此外,内存分配器可能很难找到真正大的空闲内存区域。分配较小的块可能更容易。 1GB 的内存块可能没有空间,但可能有 1000MB 的空间。

        【讨论】:

          猜你喜欢
          • 2015-06-30
          • 2015-11-22
          • 1970-01-01
          • 2018-03-11
          • 2017-06-14
          • 1970-01-01
          • 2010-12-18
          • 1970-01-01
          • 2010-12-30
          相关资源
          最近更新 更多