如何实现写入时复制?(How to implement Copy-on-Write?)

2019-09-02 13:34发布

我要实现我的自定义的C ++ String类的副本上写,我不知道如何...

我试图执行一些选择,但他们都变成了非常低效的。

感谢你们 :-)

Answer 1:

在多线程environemnt(这是大多数人现在)CoW的往往是打,而不是获得巨大的性能。 并与谨慎使用const的引用,这是没有太大的性能提升,甚至在单线程环境。

这个老DDJ文章解释是多么糟糕母牛能否在多线程环境中,即使只有一个线程 。

此外,因为其他人指出,牛字符串是真正棘手来实现,而且很容易犯错误。 这再加上他们在线程的情况下表现不佳让我真的很怀疑他们的一般用途。 一旦你开始使用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_是,你有两个原子增量型和原子减量和测试指令。

目前,涉及使用联合存储正确的,你会用它来形容一个较长的字符串数据的比特内短串的更先进的优化技术。 但是,这是更复杂,我不认为我会觉得倾向于编辑本晚一点放一个简单的例子,但你永远无法知道。



Answer 2:

有一系列关于正是这种对香草萨特的文章更出色的C ++的书。 如果你没有访问它,你可以尝试通过互联网文章如下: 第1部分 , 第2部分和第3部分 。



Answer 3:

我建议,如果一个人想实现写入时复制效率(字符串或其他),应该定义一个包装类型,这将表现为一个可变的字符串,将举行两个可为空引用一个可变的字符串(无其他参照该项目将永远存在)和可空引用到“不可改变的”字符串(引用这将永远存在以外的东西,不会尝试变异它)。 包装将始终与那些引用非空的至少一个创建的; 一旦可变项参考以往任何时候都设置为非空值(施工期间或之后),它会永远指向同一个目标。 任何时候都引用非空,不可改变的项目引用将指向这是最近完成的突变后进行一段时间的项目的副本(一个突变的过程中,不可变项参考可能会或可能不会持有参考到突变前的值)。

要阅读的对象,勾选“可变项”引用是否非空。 如果是这样,使用它。 否则,请检查“不可改变的项目”引用是否非空。 如果是这样,使用它。 否则,使用“可变项目”引用(现在将非空)。

变异对象,检查“可变项目”引用是否非空。 如果没有,复制的“不可改变的项目”参考和CompareExchange一个参考新对象的目标为“可变项目”的参考。 然后发生变异的“可变项目”参考目标和无效“不可改变的项目”引用。

克隆对象,如果克隆有望再次克隆是突变之前,检索的“不可改变的项目”的参考值。 如果为null,使“可变项目”的目标和CompareExchange参考的副本,以将新的对象不可改变的项目参考。 然后创建新的包装,其“可变项目”参考为空,并且其“不可变的项目”参考是所检索出的值(如果它是不为空)或新的项(如果它是)。

克隆对象,如果克隆有望被克隆之前突变,检索的“不可改变的项目”的参考值。 如果为null,检索“可变项目”引用。 复制者为准参考被检索的目标,并创建一个新的包装,其“可变项目”参考点的新副本,并且其“不变项”引用为null。

这两种克隆方法将是语义上相同,但选择给定情况下错了会导致额外的复制操作。 如果一个人始终选择正确的复制操作,一会就搞定最“积极”写入时复制方法的好处,但远不如线程的开销。 每个数据保持对象(例如字符串)要么是未共享可变或不可变的共享,而没有将对象这些状态之间曾经切换。 因此,人们可以如果需要消除所有“穿线/同步开销”(与直存储替换CompareExchange操作),条件是没有包装对象在多于一个线程同时使用。 两个包装对象可能拥有相同的不可变的数据持有人参考,但它们可以无视对方的存在。

需要注意的是几个拷贝操作可以使用此方法使用“积极”的方法时相比,当是必需的。 例如,如果一个新的包装与新的字符串创建,以及包装突变,并复制了六次,原包装将保持原来的琴弦固定和一成不变的一个保存数据的副本引用。 六层复制的包装只会抱到不可改变字符串的引用(两个字符串总,但如果复制以后原始字符串从未突变,积极实施可以用一个度日)。 如果原来的包装进行了突变,五六个副本,那么所有,但引用不可改变的字符串的一个沿会得到无效。 在这一点上,如果第六包装拷贝发生突变,一个积极的写入时复制实现可能会认识到它举行的字符串唯一的参考,从而决定副本是不必要的。 我所描述的实现,但是,将创建一个新的可变副本,并放弃一成不变的。 尽管有一些额外的复制操作,然而,在线程开销的减少应该在大多数情况下,足以抵消成本。 如果大多数所产生的逻辑拷贝永远不会突变,这种方法可能比总是使字符串的副本更有效。



Answer 4:

没有太多可牛。 基本上,复制,当你想改变它,让谁不希望改变它保持参照旧的实例。 你需要引用计数跟踪谁仍然引用的对象,因为你正在创建一个新的副本,你需要减少对“老”的实例计数。 快捷方式将无法进行复印时数是一个(你是唯一的参考)。

除此之外,还有很多不是可以说,除非有你面对一个具体问题。



Answer 5:

你可能想效仿“一成不变”的字符串,其他语言有(Python和C#据我所知)。

这个想法是,每个字符串是不可改变的,因此,在一根绳子上的任何工作,创建一个新的不可改变的一个......或者这是最基本的想法,以避免爆炸,你就不必再创建一个,如果有一个类似。



Answer 6:

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);
  }
}


文章来源: How to implement Copy-on-Write?