Ưu điểm của ngữ nghĩa copy-on-write


10

Tôi đang tự hỏi những giá trị có thể có của copy-on-write là gì? Đương nhiên, tôi không mong đợi ý kiến ​​cá nhân, nhưng các tình huống thực tế trong thế giới thực, nơi nó có thể mang lại lợi ích về mặt kỹ thuật và thực tế theo cách hữu hình. Và bằng cách hữu hình tôi có ý nghĩa gì đó hơn là tiết kiệm cho bạn cách gõ một &ký tự.

Để làm rõ, câu hỏi này nằm trong ngữ cảnh của các kiểu dữ liệu, trong đó việc gán hoặc sao chép xây dựng tạo ra một bản sao nông ẩn, nhưng sửa đổi nó sẽ tạo ra một bản sao sâu ẩn và áp dụng các thay đổi cho nó thay vì đối tượng ban đầu.

Lý do tôi hỏi là dường như tôi không tìm thấy bất kỳ giá trị nào của việc có COW như một hành vi ngầm định mặc định. Tôi sử dụng Qt, đã thực hiện COW cho rất nhiều kiểu dữ liệu, thực tế tất cả đều có một số lưu trữ được phân bổ động bên dưới. Nhưng làm thế nào để nó thực sự có lợi cho người dùng?

Một ví dụ:

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"

Chúng ta giành được gì khi sử dụng COW trong ví dụ này?

Nếu tất cả những gì chúng tôi dự định làm là sử dụng các hoạt động const, s1là dự phòng, cũng có thể sử dụng s.

Nếu chúng tôi có ý định thay đổi giá trị, thì COW chỉ trì hoãn việc sao chép tài nguyên cho đến lần hoạt động không phải đầu tiên, với chi phí (mặc dù tối thiểu) để tăng số lượng ref cho việc chia sẻ ngầm và tách ra khỏi bộ nhớ chia sẻ. Có vẻ như tất cả các chi phí liên quan đến COW là vô nghĩa.

Nó không khác nhiều trong bối cảnh truyền tham số - nếu bạn không có ý định sửa đổi giá trị, hãy chuyển thành tham chiếu const, nếu bạn muốn sửa đổi, bạn có thể tạo một bản sao sâu ẩn nếu bạn không muốn sửa đổi đối tượng ban đầu, hoặc chuyển qua tham chiếu nếu bạn muốn sửa đổi nó. Một lần nữa, COW có vẻ như không cần thiết mà không đạt được bất cứ điều gì và chỉ thêm một giới hạn là bạn không thể sửa đổi giá trị ban đầu ngay cả khi bạn muốn, vì mọi thay đổi sẽ tách khỏi đối tượng ban đầu.

Vì vậy, tùy thuộc vào việc bạn biết về COW hay không biết về nó, nó có thể dẫn đến mã với mục đích mơ hồ và không cần thiết, hoặc hành vi hoàn toàn khó hiểu không phù hợp với mong đợi và khiến bạn gãi đầu.

Đối với tôi có vẻ như có nhiều giải pháp hiệu quả hơn và dễ đọc hơn cho dù bạn muốn tránh một bản sao sâu không cần thiết, hoặc bạn có ý định thực hiện. Vậy đâu là lợi ích thiết thực từ COW? Tôi cho rằng phải có một số lợi ích vì nó được sử dụng trong một khuôn khổ phổ biến và mạnh mẽ như vậy.

Hơn nữa, từ những gì tôi đã đọc, COW hiện bị cấm rõ ràng trong thư viện chuẩn C ++. Không biết liệu con lừa tôi thấy trong đó có liên quan gì không, nhưng dù sao đi nữa, phải có lý do cho việc này.

Câu trả lời:


15

Sao chép trên ghi được sử dụng trong các tình huống mà bạn rất thường xuyên sẽ tạo một bản sao của đối tượng và không sửa đổi nó. Trong những tình huống đó, nó trả tiền cho chính nó.

Như bạn đã đề cập, bạn có thể vượt qua một đối tượng const và trong nhiều trường hợp là đủ. Tuy nhiên, const chỉ đảm bảo rằng người gọi không thể biến đổi nó ( const_casttất nhiên trừ khi họ ). Nó không xử lý các trường hợp đa luồng và nó không xử lý các trường hợp có các cuộc gọi lại (có thể làm thay đổi đối tượng ban đầu). Việc vượt qua một đối tượng COW theo giá trị đặt ra những thách thức trong việc quản lý các chi tiết này trên nhà phát triển API, thay vì người dùng API.

Các quy tắc mới cho C + 11 đã cấm COW std::stringnói riêng. Các vòng lặp trên một chuỗi phải bị vô hiệu nếu bộ đệm dự phòng bị tách ra. Nếu trình vòng lặp đang được triển khai dưới dạng char*(Trái ngược với a string*và chỉ mục), thì trình vòng lặp này không còn hiệu lực. Cộng đồng C ++ đã phải quyết định tần suất lặp có thể bị vô hiệu hóa và quyết định đó operator[]không phải là một trong những trường hợp đó. operator[]trên một std::stringtrả về a char&, có thể được sửa đổi. Vì vậy, operator[]sẽ cần phải tách chuỗi, vô hiệu hóa các trình vòng lặp. Đây được coi là một giao dịch kém, và không giống như các chức năng như end()cend(), không có cách nào để yêu cầu phiên bản const operator[]ngắn của const đúc chuỗi. ( liên quan ).

COW vẫn còn sống và nằm ngoài STL. Đặc biệt, tôi đã thấy nó rất hữu ích trong trường hợp người dùng API của tôi không hợp lý khi hy vọng rằng có một số vật thể nặng đằng sau thứ dường như là một vật thể rất nhẹ. Tôi có thể muốn sử dụng COW trong nền để đảm bảo họ không bao giờ phải quan tâm đến các chi tiết triển khai như vậy.


Đột biến cùng một chuỗi trong nhiều luồng có vẻ như là một thiết kế rất tệ, bất kể bạn sử dụng trình lặp hay []toán tử. Vì vậy, COW cho phép thiết kế tồi - nghe có vẻ không có lợi lắm :) Điểm trong đoạn cuối có vẻ hợp lệ, nhưng bản thân tôi không phải là một người hâm mộ tuyệt vời của hành vi ngầm - mọi người có xu hướng coi đó là điều hiển nhiên, và sau đó một thời gian khó khăn để tìm ra lý do tại sao mã không hoạt động như mong đợi và tiếp tục tự hỏi cho đến khi họ tìm ra để kiểm tra những gì ẩn đằng sau hành vi ngầm.
dtech

Đối với điểm sử dụng const_castcó vẻ như nó có thể phá vỡ COW dễ dàng như nó có thể phá vỡ bằng cách tham chiếu const. Ví dụ: QString::constData()trả về một const QChar *- const_castđó và COW sụp đổ - bạn sẽ thay đổi dữ liệu của đối tượng ban đầu.
dtech

Nếu bạn có thể trả lại dữ liệu từ COW, bạn phải tách ra trước khi thực hiện hoặc trả lại dữ liệu ở dạng vẫn nhận biết COW ( char*rõ ràng là không biết). Đối với hành vi ngầm, tôi nghĩ bạn đúng, có vấn đề với nó. Thiết kế API là sự cân bằng không đổi giữa hai thái cực. Quá ngầm, và mọi người bắt đầu dựa vào hành vi đặc biệt như thể đó là một phần của thông số kỹ thuật. Quá rõ ràng và API trở nên quá khó sử dụng khi bạn tiết lộ quá nhiều chi tiết cơ bản không thực sự quan trọng và đột nhiên được ghi vào thông số API của bạn.
Cort Ammon

Tôi tin rằng các stringlớp có hành vi COW bởi vì các nhà thiết kế trình biên dịch nhận thấy rằng một khối mã lớn đang sao chép các chuỗi thay vì sử dụng tham chiếu const. Nếu họ thêm COW, họ có thể tối ưu hóa trường hợp này và khiến nhiều người hài lòng hơn (và đó là hợp pháp, cho đến C ++ 11). Tôi đánh giá cao vị trí của họ: trong khi tôi luôn vượt qua các chuỗi của mình bằng cách tham chiếu const, tôi đã thấy tất cả những thứ linh tinh cú pháp chỉ làm mất khả năng đọc. Tôi ghét viết const std::shared_ptr<const std::string>&chỉ để nắm bắt ngữ nghĩa chính xác!
Cort Ammon

5

Đối với các chuỗi và có vẻ như nó sẽ làm giảm bớt các trường hợp sử dụng phổ biến hơn là không, vì trường hợp phổ biến cho các chuỗi thường là các chuỗi nhỏ, và ở đó chi phí của COW sẽ có xu hướng vượt xa chi phí chỉ đơn giản là sao chép chuỗi nhỏ. Một tối ưu hóa bộ đệm nhỏ có ý nghĩa hơn đối với tôi ở đó để tránh phân bổ heap trong các trường hợp như vậy thay vì các bản sao chuỗi.

Tuy nhiên, nếu bạn có một đối tượng nặng hơn, giống như Android và bạn muốn sao chép nó và chỉ cần thay thế cánh tay điều khiển học của nó, COW có vẻ khá hợp lý như một cách để giữ một cú pháp có thể thay đổi trong khi tránh phải sao chép sâu toàn bộ Android chỉ để cung cấp cho bản sao một cánh tay độc đáo. Làm cho nó trở nên bất biến như một cấu trúc dữ liệu liên tục tại thời điểm đó có thể vượt trội hơn, nhưng "COW một phần" được áp dụng trên các phần Android riêng lẻ có vẻ hợp lý cho những trường hợp này.

Trong trường hợp như vậy, hai bản sao của Android sẽ chia sẻ / ví dụ cùng một thân, chân, chân, đầu, cổ, vai, xương chậu, v.v. Dữ liệu duy nhất khác nhau giữa chúng và không được chia sẻ là cánh tay được tạo ra độc đáo cho Android thứ hai về ghi đè lên cánh tay của nó.


Điều này là tốt, nhưng nó không đòi hỏi COW, và vẫn còn chịu nhiều tác động có hại. Ngoài ra, có một nhược điểm của nó - bạn có thể thường muốn thực hiện việc kích hoạt đối tượng và tôi không có nghĩa là loại hình, nhưng sao chép một đối tượng làm ví dụ, do đó khi bạn sửa đổi đối tượng nguồn, các bản sao cũng được cập nhật. COW chỉ đơn giản là loại trừ khả năng đó, vì bất kỳ thay đổi nào đối với đối tượng "chia sẻ" đều làm mất nó.
dtech

IMO chính xác không nên "dễ dàng" đạt được, không phải với hành vi ngầm. Một ví dụ điển hình về tính đúng đắn là tính chính xác của CONST, vì nó rõ ràng và không có chỗ cho sự mơ hồ hoặc tác dụng phụ vô hình. Có một cái gì đó "dễ dàng" và tự động không bao giờ xây dựng mức độ hiểu biết thêm về cách mọi thứ hoạt động, điều này không chỉ quan trọng đối với năng suất tổng thể, mà còn loại bỏ khá nhiều khả năng hành vi không mong muốn, lý do có thể khó xác định . Tất cả mọi thứ có thể được thực hiện với COW đều dễ dàng đạt được một cách rõ ràng, và nó rõ ràng hơn.
dtech

Câu hỏi của tôi được thúc đẩy bởi một vấn đề nan giải cho dù có cung cấp COW theo mặc định trong ngôn ngữ tôi đang làm việc hay không. Sau khi cân nhắc giữa pro và con, tôi quyết định không có nó theo mặc định, nhưng là một công cụ sửa đổi có thể được áp dụng cho cả các loại mới hoặc đã có sẵn. Có vẻ như là tốt nhất của cả hai thế giới, bạn vẫn có thể có nhân chứng của COW khi bạn rõ ràng về việc muốn nó.
dtech

@ddriver Những gì chúng ta có là một thứ gì đó giống với ngôn ngữ lập trình với mô hình nút, ngoại trừ sự đơn giản các loại ngữ nghĩa giá trị sử dụng và không có ngữ nghĩa kiểu tham chiếu (có thể hơi giống với std::vector<std::string>trước khi chúng ta có emplace_backvà di chuyển ngữ nghĩa trong C ++ 11) . Nhưng về cơ bản chúng tôi cũng đang sử dụng. Hệ thống nút có thể hoặc không thể sửa đổi dữ liệu. Chúng tôi có những thứ như các nút chuyển qua không làm gì với đầu vào mà chỉ xuất ra một bản sao (chúng ở đó để tổ chức người dùng cho chương trình của anh ấy). Trong những trường hợp đó, tất cả dữ liệu được sao chép nông cho các loại phức tạp ...

@ddriver Sao chép trên ghi của chúng tôi thực sự là một loại quy trình sao chép "tạo cá thể duy nhất hoàn toàn thay đổi" . Nó làm cho nó không thể sửa đổi bản gốc. Nếu đối tượng Ađược sao chép và không có gì được thực hiện để đối tượng B, đó là một bản sao nông giá rẻ cho các loại dữ liệu phức tạp như mắt lưới. Bây giờ nếu chúng ta sửa đổi B, dữ liệu chúng ta sửa đổi Bsẽ trở thành duy nhất thông qua COW, nhưng Akhông bị ảnh hưởng (ngoại trừ một số số tham chiếu nguyên tử).
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.