如何实现Copy-on-Write?

时间:2009-10-30 10:29:40

标签: c++ string copy-on-write

我想在我的自定义C ++ String类上实现copy-on-write,我想知道如何...

我试图实施一些选项,但结果都非常低效。

谢谢你们: - )

6 个答案:

答案 0 :(得分:17)

在一个多线程的环境中(现在大部分都是这样),CoW经常是一个巨大的性能打击而不是收益。通过仔细使用const引用,即使在单线程环境中也没有太大的性能提升。

这篇旧的DDJ文章解释了just how bad CoW can be in a multithreaded environment, even if there's only one thread

此外,正如其他人所指出的那样,CoW字符串实现起来非常棘手,并且很容易出错。再加上他们在线程情况下表现不佳,这让我真的质疑他们的用处。一旦开始使用C ++ 11移动构造和移动赋值,这就变得更加真实。

但是,要回答你的问题......

以下是一些可能有助于提高性能的实现技术。

首先,将长度存储在字符串本身中。长度是经常访问的,消除指针取消引用可能会有所帮助。我只是为了保持一致,也把分配的长度放在那里。这将花费你的字符串对象有点大,但空间和复制时间的开销非常小,特别是因为这些值将更容易让编译器使用有趣的优化技巧。

这会留下一个如下所示的字符串类:

class MyString {
   ...
 private:
   class Buf {
      ...
    private:
      ::std::size_t refct_;
      char *data_;
   };

   ::std::size_t len_;
   ::std::size_t alloclen_;
   Buf *data_;
};

现在,您可以执行进一步的优化。那里的Buf类看起来并不真正包含或做得太多,这是真的。另外,它需要分配一个Buf实例和一个缓冲区来保存字符。这似乎相当浪费。因此,我们将转向一种常见的C实现技术,即弹性缓冲区:

class MyString {
   ...
 private:
   struct Buf {
      ::std::size_t refct_;
      char data_[1];
   };

   void resizeBufTo(::std::size_t newsize);
   void dereferenceBuf();

   ::std::size_t len_;
   ::std::size_t alloclen_;
   Buf *data_;
};

void MyString::resizeBufTo(::std::size_t newsize)
{
   assert((data_ == 0) || (data_->refct_ == 1));
   if (newsize != 0) {
      // Yes, I'm using C's allocation functions on purpose.
      // C++'s new is a poor match for stretchy buffers.
      Buf *newbuf = ::std::realloc(data_, sizeof(*newbuf) + (newsize - 1));
      if (newbuf == 0) {
         throw ::std::bad_alloc();
      } else {
         data_ = newbuf_;
      }
   } else { // newsize is 0
      if (data_ != 0) {
         ::std::free(data_);
         data_ = 0;
      }
   }
   alloclen_ = newsize;
}

当您以这种方式执行操作时,您可以将data_->data_视为包含alloclen_个字节,而不仅仅是1。

请记住,在所有这些情况下,您必须确保您永远不会在多线程环境中使用它,或者确保refct_是您同时拥有的类型原子增量,原子减量和测试指令。

还有一种更高级的优化技术,它涉及使用联合将短字符串存储在用于描述较长字符串的数据位内。但这更复杂,我不认为我会倾向于编辑这个以便稍后提供一个简化的例子,但你永远无法分辨。

答案 1 :(得分:10)

在Herb Sutter的More Exceptional C++书中有一系列关于这一点的文章。如果您无权访问它,可以尝试通过互联网文章:part 1part 2part 3

答案 2 :(得分:4)

我建议如果想要有效地实现copy-on-write(对于字符串或其他),应该定义一个包装类型,它将表现为一个可变的字符串,并且它将同时包含一个可变的可引用的引用string(不存在对该项的其他引用)和对“不可变”字符串的可空引用(对于不会尝试改变它的事物之外的引用永远不存在)。将始终创建Wrappers,其中至少有一个引用为非null;一旦将mutable-item引用设置为非空值(在构造期间或之后),它将永远引用相同的目标。每当两个引用都为非null时,immutable-item引用将指向在最近完成的突变之后的某个时间生成的项的副本(在变异期间,immutable-item引用可能包含也可能不包含引用到突变前的值)。

要读取对象,请检查“mutable-item”引用是否为非null。如果是这样,请使用它。否则,检查“immutable-item”引用是否为非null。如果是这样,请使用它。否则,使用“mutable item”引用(现在它将是非null)。

要改变对象,请检查“mutable-item”引用是否为非null。如果不是,则复制“immutable item”引用的目标,并将对新对象的引用CompareExchange转换为“可变项”引用。然后改变“可变项”引用的目标,并使“不可变项”引用无效。

要克隆对象,如果希望在变异之前再次克隆克隆,则检索“immutable-item”引用的值。如果为null,则复制“mutable item”目标并将对该新对象的引用CompareExchange转换为immutable-item引用。然后创建一个新的包装器,其“mutable-item”引用为null,其“immutable-item”引用是检索到的值(如果它不是null)或新项目(如果它是)。

要克隆对象,如果希望在克隆克隆之前对其进行变异,则检索“immutable-item”引用的值。如果为null,则检索“mutable-item”引用。复制检索到的任何引用的目标,并创建一个新的包装器,其“mutable-item”引用指向新副本,其“immutable-item”引用为null。

两种克隆方法在语义上是相同的,但是对于给定的情况选择错误的方法将导致额外的复制操作。如果一个人一直选择正确的复制操作,那么人们将获得“积极的”写时复制方法的大部分好处,但是线程开销要少得多。每个数据保持对象(例如字符串)将是非共享可变的或共享不可变的,并且任何对象都不会在这些状态之间切换。因此,如果需要,可以消除所有“线程/同步开销”(用直接存储替换CompareExchange操作),前提是不会同时在多个线程中使用包装器对象。两个包装器对象可能包含对同一个不可变数据持有者的引用,但它们可能会忽略彼此的存在。

请注意,使用此方法时可能需要多一些复制操作,而不是使用“激进”方法。例如,如果使用新字符串创建新的包装器,并且该包装器被更改并复制六次,则原始包装器将保留对原始字符串持有者的引用和包含数据副本的不可变引用程序。六个复制的包装器只会持有对不可变字符串的引用(总共有两个字符串,尽管如果原始字符串在复制完成后从未变异,那么一个积极​​的实现可以通过一个来实现)。如果原始包装器与六个副本中的五个一起被突变,则除了对不可变字符串的一个引用之外的所有引用都将失效。此时,如果第六个包装副本发生了变异,那么积极的写时复制实现可能会意识到它只保留了对其字符串的引用,因此决定副本是不必要的。然而,我描述的实现将创建一个新的可变副本并放弃不可变副本。尽管存在一些额外的复制操作,但是在大多数情况下,线程开销的减少应该能够抵消成本。如果生成的大多数逻辑副本从未变异,则此方法可能比始终制作字符串副本更有效。

答案 3 :(得分:3)

对于CoW来说并不多。基本上,当您想要更改它时,您可以复制,并且让任何不想更改它的人保留对旧实例的引用。您需要引用计数来跟踪谁仍在引用该对象,并且由于您正在创建新副本,因此需要减少“旧”实例的计数。一个捷径就是当计数为1时不复制(你是唯一的参考)。

除此之外,没有太多可以说的,除非你面临一个特定的问题。

答案 4 :(得分:1)

你可能想模仿其他语言所拥有的'immutable'字符串(据我所知,Python,C#)。

这个想法是每个字符串都是不可变的,因此对字符串的任何工作都会创建一个新的不可变的...或者这是基本的想法,为了避免爆炸,如果有类似的话,你不需要创建另一个。

答案 5 :(得分:0)

template <class T> struct cow {
  typedef boost::shared_ptr<T> ptr_t;
  ptr_t _data;

  ptr_t get()
  {
    return boost::atomic_load(&_data);
  }

  void set(ptr_t const& data)
  {
    boost::atomic_store(&_data, data);
  }
}