Tại sao tôi std :: di chuyển một std :: shared_ptr?


147

Tôi đã xem qua mã nguồn Clang và tôi tìm thấy đoạn trích này:

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = std::move(Value);
}

Tại sao tôi muốn std::movemột std::shared_ptr?

Có bất kỳ điểm chuyển quyền sở hữu trên một tài nguyên được chia sẻ?

Tại sao tôi không làm điều này thay vào đó?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}

Câu trả lời:


136

Tôi nghĩ rằng một điều mà các câu trả lời khác không nhấn mạnh đủ là điểm tốc độ .

std::shared_ptrsố tham chiếu là nguyên tử . tăng hoặc giảm số lượng tham chiếu đòi hỏi phải tăng hoặc giảm nguyên tử . Tốc độ này chậm hơn hàng trăm lần so với mức tăng / giảm không nguyên tử , chưa kể rằng nếu chúng ta tăng và giảm cùng một bộ đếm, chúng ta sẽ kết thúc với con số chính xác, gây lãng phí hàng tấn thời gian và tài nguyên trong quy trình.

Bằng cách di chuyển shared_ptrthay vì sao chép nó, chúng tôi "đánh cắp" số tham chiếu nguyên tử và chúng tôi vô hiệu hóa cái khác shared_ptr. "đánh cắp" số tham chiếu không phải là nguyên tử , và nó nhanh hơn hàng trăm lần so với việc sao chép shared_ptr(và gây ra sự tăng hoặc giảm tham chiếu nguyên tử ).

Xin lưu ý rằng kỹ thuật này được sử dụng hoàn toàn để tối ưu hóa. sao chép nó (như bạn đề xuất) cũng giống như chức năng tốt.


5
Có thực sự nhanh hơn hàng trăm lần? Bạn có điểm chuẩn cho điều này?
xaviersjs

1
@xaviersjs Việc chuyển nhượng yêu cầu gia tăng nguyên tử theo sau là giảm phân tử nguyên tử khi Giá trị vượt quá phạm vi. Hoạt động nguyên tử có thể mất hàng trăm chu kỳ đồng hồ. Vì vậy, có, nó thực sự là chậm hơn nhiều.
Adisak

2
@Adisak đó là lần đầu tiên tôi nghe thấy hoạt động tìm nạp và thêm ( en.wikipedia.org/wiki/Fetch-and-add ) có thể mất hàng trăm chu kỳ hơn mức tăng cơ bản. Bạn có một tài liệu tham khảo cho điều đó?
xaviersjs

2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Với các hoạt động đăng ký là một vài chu kỳ, 100 (100-300) chu kỳ cho nguyên tử phù hợp với dự luật. Mặc dù số liệu là từ năm 2013, nhưng điều này dường như vẫn đúng, đặc biệt đối với các hệ thống NUMA đa ổ cắm.
Russianfool

1
Đôi khi bạn nghĩ rằng không có luồng nào trong mã của bạn ... nhưng sau đó một số thư viện chết tiệt xuất hiện và phá hỏng nó cho bạn. Tốt hơn nên sử dụng tham chiếu const và std :: move ... nếu rõ ràng và rõ ràng rằng bạn có thể .... hơn là dựa vào số tham chiếu con trỏ.
Erik Aronesty

122

Bằng cách sử dụng, movebạn tránh tăng, và sau đó giảm ngay lập tức, số lượng cổ phiếu. Điều đó có thể giúp bạn tiết kiệm một số hoạt động nguyên tử đắt tiền về số lượng sử dụng.


1
Nó không phải là tối ưu hóa sớm?
YSC

11
@YSC không nếu ai đặt nó ở đó thực sự đã thử nó.
OrangeDog

19
@YSC Tối ưu hóa sớm là xấu nếu nó làm cho mã khó đọc hoặc duy trì hơn. Điều này không làm, ít nhất là IMO.
Angew không còn tự hào về SO

17
Thật. Đây không phải là một tối ưu hóa sớm. Nó thay vào đó là cách hợp lý để viết chức năng này.
Các cuộc đua nhẹ nhàng trong quỹ đạo

60

Các hoạt động di chuyển (như công cụ di chuyển) std::shared_ptrrất rẻ , vì về cơ bản chúng là "đánh cắp con trỏ" (từ nguồn đến đích; chính xác hơn, toàn bộ khối điều khiển trạng thái bị "đánh cắp" từ nguồn đến đích, bao gồm cả thông tin đếm tham chiếu) .

Thay vào đó, sao chép các hoạt động khi std::shared_ptrgọi tăng số tham chiếu nguyên tử (nghĩa là không chỉ ++RefCounttrên một RefCountthành viên dữ liệu số nguyên , mà ví dụ như gọi InterlockedIncrementtrên Windows), tốn kém hơn so với chỉ ăn cắp con trỏ / trạng thái.

Vì vậy, phân tích động lực đếm ref của trường hợp này một cách chi tiết:

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);

Nếu bạn chuyển sptheo giá trị và sau đó lấy một bản sao bên trong CompilerInstance::setInvocationphương thức, bạn có:

  1. Khi nhập phương thức, shared_ptrtham số được sao chép được xây dựng: số lần tăng nguyên tử ref .
  2. Bên trong cơ thể của phương pháp, bạn sao chép các shared_ptrtham số vào các thành viên dữ liệu: Ref đếm nguyên tử tăng .
  3. Khi thoát khỏi phương thức, shared_ptrtham số bị hủy: ref đếm giảm nguyên tử .

Bạn có hai mức tăng nguyên tử và một lần giảm nguyên tử, với tổng số ba hoạt động nguyên tử .

Thay vào đó, nếu bạn truyền shared_ptrtham số theo giá trị và sau đó std::movebên trong phương thức (như được thực hiện đúng trong mã của Clang), bạn có:

  1. Khi nhập phương thức, shared_ptrtham số được sao chép được xây dựng: số lần tăng nguyên tử ref .
  2. Bên trong cơ thể của phương pháp, bạn std::movecác shared_ptrtham số vào các thành viên dữ liệu: số lượng ref không không thay đổi! Bạn chỉ đang ăn cắp con trỏ / trạng thái: không có hoạt động đếm số nguyên tử đắt tiền nào được tham gia.
  3. Khi thoát khỏi phương thức, shared_ptrtham số bị hủy; nhưng vì bạn đã chuyển sang bước 2, nên không có gì để hủy, vì shared_ptrtham số không còn trỏ đến bất cứ điều gì nữa. Một lần nữa, không có sự suy giảm nguyên tử xảy ra trong trường hợp này.

Điểm mấu chốt: trong trường hợp này bạn chỉ nhận được một lần tăng số nguyên tử, tức là chỉ một thao tác nguyên tử .
Như bạn có thể thấy, điều này tốt hơn nhiều so với hai mức tăng nguyên tử cộng với một lần giảm nguyên tử (cho tổng số ba thao tác nguyên tử) cho trường hợp sao chép.


1
Cũng đáng chú ý: tại sao họ không vượt qua tham chiếu const và tránh toàn bộ std :: di chuyển công cụ? Bởi vì pass-by-value cũng cho phép bạn truyền trực tiếp vào một con trỏ thô và sẽ chỉ có một shared_ptr được tạo.
Joseph Ireland

@JosephIreland Bởi vì bạn không thể di chuyển một tài liệu tham khảo const
Bruno Ferreira

2
@JosephIreland vì nếu bạn gọi như compilerInstance.setInvocation(std::move(sp));vậy thì sẽ không có gia tăng . Bạn có thể có hành vi tương tự bằng cách thêm một quá tải mất một lần shared_ptr<>&&nhưng tại sao lại trùng lặp khi bạn không cần.
ratchet quái dị

2
@BrunoFerreira Tôi đã trả lời câu hỏi của riêng tôi. Bạn sẽ không cần phải di chuyển nó bởi vì nó là một tài liệu tham khảo, chỉ cần sao chép nó. Vẫn chỉ có một bản thay vì hai. Lý do họ không làm điều đó là vì nó sẽ sao chép một cách không cần thiết shared_ptrs mới được xây dựng, ví dụ như từ setInvocation(new CompilerInvocation), hoặc như ratchet đã đề cập , setInvocation(std::move(sp)). Xin lỗi nếu bình luận đầu tiên của tôi không rõ ràng, tôi thực sự đã đăng nó một cách tình cờ, trước khi tôi viết xong và tôi quyết định chỉ để lại nó
Joseph Ireland

22

Sao chép shared_ptrliên quan đến việc sao chép con trỏ đối tượng trạng thái bên trong của nó và thay đổi số tham chiếu. Di chuyển nó chỉ liên quan đến việc hoán đổi con trỏ đến bộ đếm tham chiếu nội bộ và đối tượng sở hữu, vì vậy nó nhanh hơn.


16

Có hai lý do để sử dụng std :: move trong tình huống này. Hầu hết các phản hồi đề cập đến vấn đề tốc độ, nhưng bỏ qua vấn đề quan trọng là thể hiện ý định của mã rõ ràng hơn.

Đối với std :: shared_ptr, std :: di chuyển rõ ràng biểu thị việc chuyển quyền sở hữu của người được chỉ định, trong khi thao tác sao chép đơn giản sẽ thêm chủ sở hữu bổ sung. Tất nhiên, nếu chủ sở hữu ban đầu sau đó từ bỏ quyền sở hữu của họ (chẳng hạn như bằng cách cho phép std :: shared_ptr của họ bị hủy), thì việc chuyển quyền sở hữu đã được thực hiện.

Khi bạn chuyển quyền sở hữu với std :: move, rõ ràng điều gì đang xảy ra. Nếu bạn sử dụng một bản sao bình thường, rõ ràng hoạt động dự định là chuyển khoản cho đến khi bạn xác minh rằng chủ sở hữu ban đầu ngay lập tức từ bỏ quyền sở hữu. Như một phần thưởng, việc triển khai hiệu quả hơn là có thể, vì việc chuyển quyền sở hữu nguyên tử có thể tránh được trạng thái tạm thời khi số lượng chủ sở hữu tăng thêm một (và người tham gia thay đổi về số lượng tham chiếu).


Chính xác những gì tôi đang tìm kiếm. Ngạc nhiên làm thế nào các câu trả lời khác bỏ qua sự khác biệt quan trọng về ngữ nghĩa này. con trỏ thông minh là tất cả về quyền sở hữu.
qweruiop

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.