Méritos da semântica de copiar na gravação

10

Pergunto-me que méritos possíveis o copy-on-write tem? Naturalmente, não espero opiniões pessoais, mas cenários práticos do mundo real em que pode ser técnica e praticamente benéfica de maneira tangível. Por tangível, quero dizer algo mais do que poupar a digitação de um &personagem.

Para esclarecer, essa pergunta está no contexto de tipos de dados, em que a construção de atribuição ou cópia cria uma cópia superficial implícita, mas as modificações nela criam uma cópia profunda implícita e aplicam as alterações a ela, em vez do objeto original.

A razão pela qual estou perguntando é que não consigo encontrar nenhum mérito de ter a COW como um comportamento implícito padrão. Eu uso o Qt, que implementou o COW para muitos tipos de dados, praticamente todos os quais possuem algum armazenamento alocado dinamicamente subjacente. Mas como isso realmente beneficia o usuário?

Um exemplo:

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"

O que ganhamos usando o COW neste exemplo?

Se tudo o que pretendemos fazer é usar operações const, s1é redundante e pode ser usado s.

Se pretendemos alterar o valor, o COW apenas atrasa a cópia do recurso até a primeira operação não const, ao custo (ainda que mínimo) de incrementar a contagem de ref para o compartilhamento implícito e a desanexação do armazenamento compartilhado. Parece que toda a sobrecarga envolvida na COW é inútil.

Não é muito diferente no contexto da passagem de parâmetros - se você não pretende modificar o valor, passe como referência const; se quiser modificar, faça uma cópia implícita e profunda se não quiser modificar o objeto original ou passe por referência, se desejar modificá-lo. Novamente, o COW parece uma sobrecarga desnecessária que não alcança nada e apenas adiciona uma limitação de que você não pode modificar o valor original, mesmo que queira, pois qualquer alteração será destacada do objeto original.

Portanto, dependendo se você conhece a COW ou não o conhece, pode resultar em código com intenção obscura e sobrecarga desnecessária, ou comportamento completamente confuso que não corresponde às expectativas e deixa você coçando a cabeça.

Para mim, parece que existem soluções mais eficientes e legíveis, se você deseja evitar uma cópia profunda desnecessária ou se pretende fazer uma. Então, onde está o benefício prático da vaca? Suponho que deve haver algum benefício, pois é usado em uma estrutura tão popular e poderosa.

Além disso, pelo que li, o COW agora é explicitamente proibido na biblioteca padrão do C ++. Não sei se os truques que vejo nele têm algo a ver com isso, mas, de qualquer forma, deve haver uma razão para isso.

dtech
fonte

Respostas:

15

A cópia na gravação é usada em situações em que você frequentemente cria uma cópia do objeto e não a modifica. Nessas situações, ele se paga.

Como você mencionou, você pode passar um objeto const e, em muitos casos, isso é suficiente. No entanto, const apenas garante que o chamador não pode modificá-lo (a menos que const_cast, é claro). Ele não lida com casos de multithreading e não lida com casos em que há retornos de chamada (que podem alterar o objeto original). Passar um objeto COW por valor coloca os desafios de gerenciar esses detalhes no desenvolvedor da API, e não no usuário da API.

As novas regras para C + 11 proíbem COW, std::stringem particular. Os iteradores em uma cadeia de caracteres devem ser invalidados se o buffer de backup for desanexado. Se o iterador estava sendo implementado como um char*(ao contrário de a string*e um índice), esses iteradores não são mais válidos. A comunidade C ++ teve que decidir com que frequência os iteradores poderiam ser invalidados, e a decisão foi que operator[]não deveria ser um desses casos. operator[]em um std::stringretorna a char&, que pode ser modificado. Portanto, operator[]seria necessário desanexar a sequência, invalidando iteradores. Isso foi considerado um comércio ruim e, ao contrário de funções como end()e cend(), não há como solicitar a versão operator[]const, com exceção de const, lançando a string. ( relacionado ).

A vaca ainda está viva e bem fora do STL. Em particular, achei muito útil nos casos em que não é razoável para um usuário de minhas APIs esperar que exista algum objeto pesado por trás do que parece ser um objeto muito leve. Talvez eu queira usar o COW em segundo plano para garantir que eles nunca precisem se preocupar com esses detalhes de implementação.

Cort Ammon
fonte
A mutação da mesma sequência em vários threads parece um design muito ruim, independentemente de você usar iteradores ou o []operador. Portanto, o COW permite um design ruim - que não parece muito benéfico :) O ponto do último parágrafo parece válido, mas eu próprio não sou um grande fã de comportamento implícito - as pessoas tendem a considerá-lo um dado adquirido e depois têm é difícil descobrir por que o código não funciona como o esperado e fica pensando até descobrirem o que está oculto por trás do comportamento implícito.
DTECH
Quanto ao ponto de uso, const_castparece que ele pode quebrar o COW com a mesma facilidade que ele pode quebrar a passagem por referência const. Por exemplo, QString::constData()retorna um const QChar *- const_castque e a COW entra em colapso - você modifica os dados do objeto original.
DTECH
Se você pode retornar dados de uma vaca, você deve desanexar antes de fazê-lo ou retornar os dados em um formulário que ainda esteja ciente da vaca ( char*obviamente não está ciente). Quanto ao comportamento implícito, acho que você está certo, há problemas com ele. O design da API é um equilíbrio constante entre os dois extremos. Implícito demais, e as pessoas começam a confiar em comportamentos especiais como se isso fosse parte de fato das especificações. Muito explícito, e a API se torna muito pesada à medida que você expõe muitos detalhes subjacentes que não eram realmente importantes e, de repente, são gravados nas especificações da API.
Cort Ammon
Acredito que as stringclasses tenham comportamento COW porque os designers do compilador perceberam que um grande corpo de código estava copiando seqüências de caracteres em vez de usar const-reference. Se eles adicionassem o COW, eles poderiam otimizar esse caso e fazer mais pessoas felizes (e isso era legal até C ++ 11). Aprecio a posição deles: embora eu sempre passei minhas cordas por referência const, vi todo esse lixo sintático que apenas prejudica a legibilidade. Eu odeio escrever const std::shared_ptr<const std::string>&apenas para capturar a semântica correta!
Cort Ammon
5

Para seqüências de caracteres e coisas do tipo, isso parece pessimizar casos de uso mais comuns do que não, pois o caso comum de seqüências de caracteres geralmente é de seqüências pequenas, e a sobrecarga do COW tende a compensar muito o custo de simplesmente copiar a sequência de caracteres pequena. Uma pequena otimização de buffer faz muito mais sentido para mim, para evitar a alocação de heap nesses casos, em vez das cópias de string.

Porém, se você tem um objeto mais pesado, como um andróide, e deseja copiá-lo e apenas substituir seu braço cibernético, a COW parece bastante razoável como uma maneira de manter uma sintaxe mutável, evitando a necessidade de copiar profundamente todo o andróide apenas para dê à cópia um braço único. Torná-lo imutável como uma estrutura de dados persistente nesse ponto pode ser superior, mas uma "COW parcial" aplicada em partes individuais do android parece razoável para esses casos.

Nesse caso, as duas cópias do androide compartilhariam / instariam o mesmo tronco, pernas, pés, cabeça, pescoço, ombros, pélvis etc. Os únicos dados que seriam diferentes entre eles e não compartilhados são o braço que foi feito exclusivo para o segundo androide sobrescrevendo seu braço.


fonte
Tudo isso é bom, mas não exige COW e ainda está sujeito a muita implicação prejudicial. Além disso, há uma desvantagem: você pode querer instanciar objetos, e não quero dizer instanciar tipos, mas copiar um objeto como uma instância; portanto, quando você modifica o objeto de origem, as cópias também são atualizadas. O COW simplesmente exclui essa possibilidade, pois qualquer alteração em um objeto "compartilhado" a desanexa.
DTECH
Correção A IMO não deve ser "fácil" de alcançar, não com comportamento implícito. Um bom exemplo de correção é a correção CONST, pois é explícita e não deixa espaço para ambiguidades ou efeitos colaterais invisíveis. Ter algo como esse "fácil" e automático nunca aumenta esse nível extra de entendimento de como as coisas funcionam, o que não é apenas importante para a produtividade geral, mas elimina praticamente a possibilidade de comportamento indesejado, cuja razão pode ser difícil de identificar. . Tudo o que é possibilitado implicitamente com o COW também é fácil de ser explicitado, e é mais claro.
DTECH
Minha pergunta foi motivada por um dilema: fornecer ou não COW por padrão no idioma em que estou trabalhando. Depois de ponderar os prós e contras, decidi não tê-lo por padrão, mas como um modificador que pode ser aplicado a tipos novos ou já existentes. Parece o melhor dos dois mundos, você ainda pode ter a implicação de COW quando é explícito quanto a desejá-lo.
DTECH
@ddriver O que temos é algo parecido com uma linguagem de programação com o paradigma nodal, exceto pela simplicidade o tipo de nós usa semântica de valor e nenhuma semântica de tipo de referência (talvez algo parecido com std::vector<std::string>antes de termos emplace_backe mover a semântica em C ++ 11) . Mas também estamos basicamente usando instanciamento. O sistema de nós pode ou não modificar os dados. Temos coisas como nós de passagem que não fazem nada com a entrada, mas apenas produzem uma cópia (eles estão lá para a organização do usuário de seu programa). Nesses casos, todos os dados é superficial copiado para tipos complexos ...
@ddriver Nosso copy-on-write é efetivamente um tipo de processo de cópia "tornar a instância única implicitamente em mudança" . Torna impossível modificar o original. Se o objeto Aé copiado e nada é feito para o objeto B, é uma cópia superficial barata para tipos de dados complexos, como malhas. Agora, se modificarmos B, os dados em que modificamos Bse tornam únicos por meio do COW, mas Asão intocados (exceto por algumas contagens de referência atômica).