我正在尝试制作rope数据结构。它是一种二叉树,即递归数据结构。
绳索的目的是分裂和连接应该快速,这意味着你避免复制整个绳索。
因此,例如,用户应该能够说rope1 + rope2
并期望得到〜对数时间的结果。
然而,这提出了一个问题:
如果绳索被修改,其父母也会被间接修改
因为我的目标是让rope
成为string
的替代品,所以这是不可接受的。
我的解决方案是:只要对rope
进行任何类型的更改,我就会创建一个 new 节点,稍作修改,保留原有的旧版本。
理论上,我认为这样可以很好地运作。
但实际上,它涉及一个堆分配(几乎?)字符串的每个修改。
即使是单字符更改也会导致新的堆对象,这不仅会自身缓慢,而且还会显着降低内存位置,从而对性能产生负面影响。
我该如何解决这个问题呢?
答案 0 :(得分:5)
传统的方法是写时复制:为此,您需要重新计算每个已分配的节点。
如果修改后的节点的引用计数为1(没有其他人引用它),则无需复制它。
这方面的实际问题是有用地隔离变异与非变异操作:
char& rope::operator[] (std::string::pos)
可以改变引用的char,但是当实际上不会时,没有任何简单的方法可以强制选择const重载。这意味着您要么必须假设发生突变,并且可能触发不必要的副本,或者返回一些重载char转换和赋值的代理。
这种方法是为std::string
的早期实现而尝试的(其中一个字符串相当于一个单节点的绳索)iirc,并且失宠了;部分是因为突变问题,部分是因为如果您不得不担心多线程,COW和所需的引用计数会变得越来越昂贵。
正如你所说,如果你的节点包含两种独立的状态类型,那么绳索会有一个额外的问题:它自己的字符串和对其子节点的引用(因为这会导致引用计数/子引用突变以传播树)。
如果相反,字符仅存储在叶节点上,并且您执行非叶节点的完整副本(因此每条绳索都有自己的“目录结构”),您仍然可以避免复制字符和refcounted共享状态要简单得多。
这会得到你想要的对数时间连接吗?也许不是:
看起来是否接近线性或对数时间取决于递增引用计数与复制目录树的相对成本。
如果没有这个,你可以获得快速连接,但是如果必须在树上传播COW操作,任意字符访问可能(不可预测地)退化为对数时间。
我想,如果它适合你的用例,你可以实现移动连接:这可能会给出恒定的时间添加,你仍然可以避免额外的COW复杂性。
答案 1 :(得分:3)
但实际上,它涉及(几乎?)每次修改字符串的堆分配。
如果你想避免频繁的堆分配性能问题,那么我建议为你的类使用一个内存池来分配一块内存,只需要在操作系统满了时请求一个新的分配,这应该是很少发生。然后,您可以优化对内存池的访问,以分配像char
等小块数据类型。
Andrei Alexandrescu在他的“现代C ++设计”一书中有一个很好的小块内存分配器的例子。