关于c#:是否存在Rope数据结构比字符串生成器更高效的情况

Is there any scenario where the Rope data structure is more efficient than a string builder

Related to this question, based
on a comment of user Eric
Lippert.

有没有比字符串生成器更有效的Rope数据结构的场景?有人认为,在典型情况下,在速度方面,rope数据结构几乎永远不会比本地字符串或字符串生成器操作更好,所以我很好奇看到实际情况下,rope确实更好。


SGI C++实现的文档会对大O行为的一些细节进行详细说明,这些常数是有启发性的。

他们的文档假设涉及非常长的字符串,作为参考的示例讨论10 MB的字符串。很少有程序能够处理这样的事情,而且对于许多有这样的需求的问题,将它们改写为基于流的,而不是在可能的情况下要求提供完整的字符串,这将导致显著的优越的结果。因此,当您能够适当地将rope视为区段(本身为rope)而不仅仅是字符序列时,rope用于对多兆字符序列的非流式操作。

重要优点:

  • 连接/插入几乎成为常量时间操作
  • 某些操作可能会重用以前的绳索部分,以允许在内存中共享。
    • 注意.NET字符串与Java字符串不同,它不会在子字符串上共享字符缓冲区——从内存占用的角度考虑其优缺点。绳索可以避免这种问题。
  • 绳索允许在需要之前延迟加载子环。
    • 注意,这很难纠正,很容易由于访问的过度迫切性而变得毫无意义,并且需要使用代码将其视为一条绳子,而不是字符序列。

重大缺点:

  • 随机读访问变为O(log n)
  • 顺序读取访问的常量因子似乎介于5和10之间。
  • 有效地使用API需要将其视为一根绳子,而不仅仅是将绳子作为"普通"字符串API的后备实现。

这导致了一些"明显"的用途(SGI首先明确提到)。

  • 在大文件上编辑缓冲区,方便撤消/重做
    • 请注意,在某个时刻,您可能需要将更改写入磁盘,这涉及到整个字符串的流式处理,因此只有当大多数编辑将主要驻留在内存中而不需要频繁的持久性时(例如通过autosave函数),这才有用。
  • 对DNA片段的操作,其中发生了大量的操作,但实际上很少发生输出。
  • 多线程算法,改变字符串的局部子段。理论上,这种情况可以被分割成单独的线程和核心,而不需要获取子部分的本地副本,然后重新组合它们,从而节省大量的内存,同时避免了最后代价高昂的串行组合操作。

有些情况下,字符串中特定于域的行为可以与对rope实现的相对简单的扩充相结合,以允许:

  • 具有大量常见子串的只读字符串可以通过简单的Interning实现显著的内存节省。
  • 具有稀疏结构或显著局部重复的字符串可以进行长度编码,同时仍然允许合理的随机访问级别。
  • 其中,子字符串边界本身就是可以存储信息的"节点",不过如果很少修改这些结构,但经常读取它们,那么最好将它们作为基数trie来完成。

正如您从所列的例子中看到的,所有这些都属于"利基"类别。此外,如果您愿意/能够将该算法重写为流处理操作,那么可能会有一些更好的选择。


这个问题的简短回答是肯定的,这不需要什么解释。当然,在某些情况下,rope数据结构比字符串生成器更有效。它们的工作方式不同,因此更适合于不同的用途。

(从C的角度看)

在某些情况下,作为二叉树的rope数据结构更好。当您查看非常大的字符串值时(假设100+MB的XML来自SQL),Rope数据结构可以使整个进程远离大型对象堆,当字符串对象通过85000个字节时,它会在其中命中它。

如果您查看的是5-1000个字符的字符串,那么它可能不会提高足够的性能。这是数据结构的另一个例子,它是为5%有极端情况的人设计的。


第10届ICFP编程竞赛基本上依赖于人们使用rope数据结构进行有效解决。这是获得一个在合理时间运行的虚拟机的大技巧。

如果有很多前缀的话,绳子是很好的(显然"prepending"这个词是由IT人员组成的,不是一个合适的词!)而且对于插入可能更好;StringBuilder使用连续内存,因此只对附加有效。

因此,StringBuilder非常适合通过附加片段来构建字符串——这是非常正常的用例。由于开发人员需要做很多工作,所以架线构建器是一种非常主流的技术。

Ropes非常适合编辑缓冲区,例如企业级文本区域背后的数据结构。因此(放松绳索,例如一个链接的行列表,而不是一个二叉树)在UI控件世界中非常常见,但是这些控件的开发人员和用户并不经常接触到它。

你真的需要大量的数据和搅动来实现绳索的放线——处理器非常擅长流操作,如果你有RAM,那么简单地说,预处理的realloc在正常使用情况下是可以接受的。上面提到的比赛是我唯一一次看到它需要。


大多数高级文本编辑器将文本体表示为"一种绳索"(尽管在实现中,叶通常不是单个字符,而是文本运行),主要是为了提高大型文本的频繁插入和删除。

通常,StringBuilder针对追加进行了优化,并尝试在不过度分配的情况下最小化重新分配的总数。典型的保证是(log2 n分配,小于内存的2.5x)。通常情况下,字符串只生成一次,然后可以使用相当长一段时间而不需要修改。

Rope针对频繁的插入和删除进行了优化,并尝试最小化复制的数据量(通过更多的分配)。在线性缓冲区实现中,每个插入和删除都变为O(N),通常需要表示单个字符的插入。


JavaScript虚拟机通常使用绳子作为字符串。

希格斯JavaScript虚拟机开发人员Maxime Chevalier Boisvert说:

In JavaScript, you can use arrays of strings and eventually
Array.prototype.join to make string concatenation reasonably fast,
O(n), but the"natural" way JS programmers tend to build strings is to
just append using the += operator to incrementally build them. JS
strings are immutable, so if this isn't optimized internally,
incremental appending is O(n2 ). I think it's probable that ropes were
implemented in JS engines specifically because of the SunSpider
benchmarks which do string appending. JS engine implementers used
ropes to gain an edge over others by making something that was
previously slow faster. If it wasn't for those benchmarks, I think
that cries from the community about string appending performing poorly
may have been met with"use Array.prototype.join, dummy!".

也。