写时复制语义的优点


10

我想知道写时复制有什么优点?当然,我并不期望个人观点,而是希望在实际操作中以切实可行的方式在技术上和实践中受益。实际上,我的意思不只是节省&字符输入。

要澄清的是,此问题是在数据类型的上下文中进行的,在赋值或副本构造中创建一个隐式浅表副本,但对其进行修改会创建一个隐式深表副本,并将更改应用于它而不是原始对象。

我问的原因是,我似乎没有发现将COW作为默认隐式行为的任何优点。我使用Qt,它为许多数据类型实现了COW,实际上所有数据类型都具有一些动态分配的底层存储。但是它如何真正使用户受益呢?

一个例子:

QString s("some text");
QString s1 = s; // now both s and s1 internally use the same resource

qDebug() << s1; // const operation, nothing changes
s1[o] = z; // s1 "detaches" from s, allocates new storage and modifies first character
           // s is still "some text"

在此示例中,使用COW可以赢得什么?

如果我们打算做的只是使用const操作,那s1是多余的,不妨使用s

如果我们打算更改该值,则COW仅将资源复制延迟到第一个非常量操作为止,其代价是(为最小)增加隐式共享的引用计数并从共享存储中分离。看起来COW涉及的所有开销都是毫无意义的。

在参数传递的上下文中没有太大区别-如果您不打算修改值,则以const引用的形式传递,如果您要修改,则可以隐式深层复制(如果您不想修改)原始对象,如果要对其进行修改,则通过引用传递。同样,COW似乎是不必要的开销,什么也做不了,仅增加了一个限制,即即使您愿意也不能修改原始值,因为任何更改都将脱离原始对象。

因此,取决于您是否了解COW或遗忘了COW,它可能导致意图含糊的代码和不必要的开销,或者导致完全混乱的行为,与预期不符,使您抓狂。

在我看来,无论您是想要避免不必要的深复制还是打算制作一个深复制,似乎都有更有效,更易读的解决方案。那么,COW的实际好处在哪里呢?我认为必须有一些好处,因为它在如此流行而强大的框架中使用。

此外,据我了解,C ++标准库中现已明确禁止使用COW。不知道我看到的缺点是否与它有关,但是无论哪种方式,都一定有原因。

Answers:


15

写时复制用于经常创建对象副本而不修改它的情况。在这种情况下,它会收回成本。

如前所述,您可以传递const对象,并且在许多情况下就足够了。但是,const仅保证调用者无法对其进行突变(const_cast当然,除非他们)。它不处理多线程情况,也不处理存在回调(可能会使原始对象发生变化)的情况。按值传递COW对象带来了在API开发人员(而不是API用户)上管理这些详细信息的挑战。

C + 11的新规则std::string尤其禁止COW 。如果分离了后备缓冲区,则字符串上的迭代器必须无效。如果迭代器被实现为a char*(与a string*和索引相对),则该迭代器不再有效。C ++社区必须决定使迭代器失效的频率,并且决定operator[]不应该是其中一种情况。 operator[]在上std::string返回char&,可以修改。因此,operator[]将需要分离字符串,从而使迭代器无效。这被认为是一项糟糕的交易,并且与诸如end()和之类的函数不同cend(),没有办法要求operator[]缺少const转换字符串的const版本。(相关)。

COW仍然存在,并且不在STL之外。特别是,在我的API用户不合理地期望看起来很轻量的对象后面有一些重量级对象的情况下,我发现它非常有用。我不妨在后台使用COW,以确保他们永远不必担心此类实现细节。


不管您使用迭代器还是[]运算符,在多个线程中对同一字符串进行突变似乎都是很糟糕的设计。因此,COW启用了错误的设计-听起来似乎没有多大好处:)上一段中的观点似乎是正确的,但我本人并不是隐式行为的忠实拥护者-人们倾向于将其视为理所当然,然后很难弄清楚为什么代码无法按预期工作,并一直想知道直到他们找出来检查隐式行为背后隐藏的内容。
dtech 2015年

至于使用的观点const_cast似乎可以像打破const引用传递一样容易地打破COW。例如,QString::constData()返回const QChar *- const_cast且COW折叠-您将突变原始对象的数据。
dtech

如果您可以从COW中返回数据,则必须先进行分离,或者以仍可识别COW(char*显然不知道)的格式返回数据。至于隐性行为,我认为你是对的,这有问题。API设计是两个极端之间的恒定平衡。太含蓄了,人们开始依赖特殊行为,就好像它实际上是规范的一部分一样。过于露骨,并且由于暴露了很多并不重要的底层细节,API变得笨拙,并突然写入了API规范。
Cort Ammon 2015年

我相信string类具有COW行为,因为编译器设计人员注意到大量代码正在复制字符串而不是使用const-reference。如果他们添加了COW,他们可以优化这种情况并使更多的人满意(这是合法的,直到C ++ 11为止)。我很欣赏它们的位置:尽管我总是通过const引用传递字符串,但我看到了所有语法上的垃圾只会损害可读性。我讨厌写const std::shared_ptr<const std::string>&只是为了捕捉正确的语义!
Cort Ammon 2015年

5

对于字符串来说,这似乎比不使用更常见的用例更为悲观,因为字符串的常见用例通常是小字符串,因此,COW的开销往往远远超过简单复制小字符串的成本。对于我来说,小缓冲区优化对我来说更有意义,以避免在这种情况下避免堆分配而不是字符串副本。

但是,如果您有一个较重的对象(例如android),并且想要复制它并只替换其控制论臂,则COW似乎很合理,因为它可以保持可变的语法,同时又无需将整个android深度复制到给副本一个独特的手臂。在那个时候使其成为不变的数据结构可能会更好,但是在这些情况下,将“部分COW”应用于各个android部件似乎是合理的。

在这种情况下,Android的两个副本将共享/实例化相同的躯干,腿,脚,头,脖子,肩膀,骨盆等。它们之间唯一不共享的数据是手臂对于第二个覆盖其手臂的android而言是唯一的。


这一切都很好,但是它不需要COW,并且仍然受到很多有害隐式的约束。此外,它还有一个缺点-您可能经常想进行对象实例化,而我并不是说要进行类型实例化,而是将对象复制为实例,因此,当您修改源对象时,副本也会被更新。COW只是排除了这种可能性,因为对“共享”对象的任何更改都会使它脱离。
dtech

正确性IMO不应“容易”实现,而不能隐含行为。正确性的一个很好的例子是CONST正确性,因为它是明确的,没有歧义或看不见的副作用的余地。拥有这种“简单”和自动的东西永远不会建立对事情如何运作的额外理解,这不仅对整体生产力很重要,而且几乎消除了不良行为的可能性,而这种不良行为的原因可能很难查明。 。用COW隐式实现的一切也很容易显式实现,并且更加清晰。
dtech 2015年

我的问题是由一个困境引起的,即是否默认以我正在使用的语言提供COW。在权衡利弊之后,我决定默认情况下不使用它,而是将其作为可同时应用于新类型或现有类型的修饰符。似乎两全其美,当您明确想要COW时,您仍然可以将其隐含在COW中。
dtech

@ddriver我们所拥有的是一个类似于与节点范式编程语言,除了简单的节点类型的使用值语义和没有提到型语义(也许有点类似于std::vector<std::string>我们之前曾emplace_back在移至C ++语义11) 。但是我们基本上也使用实例化。节点系统可能会也可能不会修改数据。我们有直通节点之类的东西,它们对输入不执行任何操作,而仅输出一个副本(它们在那里供用户程序使用)。在那种情况下,所有数据都会被浅表复制以用于复杂类型...

@ddriver我们的写时复制实际上是一种“使实例在更改时隐式唯一”的复制过程。这样就无法修改原件。如果对象A被复制而对对象不执行任何操作,则对于B诸如网格之类的复杂数据类型而言,这是便宜的浅表副本。现在,如果我们修改B,我们修改的数据B将通过COW变成唯一的,但A未被修改(某些原子引用计数除外)。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.