Mengapa saya std :: memindahkan std :: shared_ptr?

148

Saya telah mencari melalui kode sumber Dentang dan saya menemukan potongan ini:

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

Mengapa saya ingin std::movesebuah std::shared_ptr?

Apakah ada titik mentransfer kepemilikan pada sumber daya bersama?

Kenapa aku tidak melakukan ini saja?

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

Jawaban:

137

Saya pikir satu hal yang tidak ditekankan oleh jawaban yang lain adalah titik kecepatan .

std::shared_ptrjumlah referensi adalah atom . menambah atau mengurangi jumlah referensi membutuhkan kenaikan atau penurunan atom . Ini seratus kali lebih lambat daripada kenaikan / penurunan non-atom , belum lagi bahwa jika kita menambah dan mengurangi penghitung yang sama kita menghasilkan angka yang tepat, membuang satu ton waktu dan sumber daya dalam proses.

Dengan memindahkan shared_ptralih - alih menyalinnya, kami "mencuri" jumlah referensi atom dan kami membatalkan yang lain shared_ptr. "mencuri" jumlah referensi bukan atom , dan seratus kali lebih cepat daripada menyalin shared_ptr(dan menyebabkan kenaikan atau penurunan referensi atom ).

Perhatikan bahwa teknik ini hanya digunakan untuk optimasi. menyalinnya (seperti yang Anda sarankan) sama baiknya dengan fungsi.

David Haim
sumber
5
Apakah ini seratus kali lebih cepat? Apakah Anda memiliki tolok ukur untuk ini?
xaviersjs
1
@xaviersjs Tugas ini membutuhkan kenaikan atom diikuti oleh penurunan atom ketika Nilai keluar dari ruang lingkup. Operasi atom dapat mengambil ratusan siklus clock. Jadi ya, itu jauh lebih lambat.
Adisak
2
@Adisak itulah yang pertama kali saya dengar operasi ambil dan tambahkan ( en.wikipedia.org/wiki/Fetch-and-add ) dapat membutuhkan ratusan siklus lebih dari peningkatan dasar. Apakah Anda punya referensi untuk itu?
xaviersjs
2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Dengan operasi register menjadi beberapa siklus, siklus 100-an (100-300) untuk atom sesuai dengan tagihan. Meskipun metrik berasal dari 2013, ini tampaknya masih berlaku terutama untuk sistem NUMA multi-socket.
Russianfool
1
Terkadang Anda berpikir tidak ada threading dalam kode Anda ... tapi kemudian beberapa perpustakaan sialan datang dan merusaknya untuk Anda. Lebih baik menggunakan referensi const dan std :: move ... jika jelas dan jelas bahwa Anda dapat .... daripada mengandalkan jumlah referensi pointer.
Erik Aronesty
123

Dengan menggunakan moveAnda menghindari peningkatan, dan kemudian segera menurun, jumlah saham. Itu mungkin menghemat beberapa operasi atom mahal pada hitungan penggunaan.

Bo Persson
sumber
1
Apakah ini bukan optimasi prematur?
YSC
11
@YSC tidak jika siapa pun yang meletakkannya di sana benar-benar mengujinya.
OrangeDog
19
@YSC Optimalisasi prematur adalah jahat jika membuat kode lebih sulit dibaca atau dikelola. Yang ini tidak, setidaknya IMO.
Angew tidak lagi bangga dengan SO
17
Memang. Ini bukan optimasi prematur. Ini bukan cara yang masuk akal untuk menulis fungsi ini.
Lightness Races di Orbit
60

Langkah operasi (seperti langkah konstruktor) untuk std::shared_ptryang murah , karena mereka pada dasarnya adalah "mencuri pointer" (dari sumber ke tujuan, untuk lebih tepatnya, blok kontrol negara seluruh adalah "dicuri" dari sumber ke tujuan, termasuk informasi jumlah referensi) .

Alih-alih menyalin operasi pada std::shared_ptrpanggilan peningkatan referensi nomor atom (yaitu tidak hanya ++RefCountpada anggota RefCountdata integer , tetapi misalnya memanggil InterlockedIncrementpada Windows), yang lebih mahal daripada hanya mencuri pointer / negara.

Jadi, analisis dinamika hitungan ref dalam kasus ini secara terperinci:

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

Jika Anda melewati spnilai dan kemudian mengambil salinan di dalam CompilerInstance::setInvocationmetode, Anda memiliki:

  1. Saat memasukkan metode, shared_ptrparameter dibuat salinan: ref count atomic increment .
  2. Di dalam tubuh metode ini, Anda menyalin yang shared_ptrparameter ke dalam data anggota: REF menghitung atom kenaikan .
  3. Saat keluar dari metode, shared_ptrparameter dirusak: ref count atomic decrement .

Anda memiliki dua peningkatan atom dan satu penurunan atom, dengan total tiga operasi atom .

Sebaliknya, jika Anda melewatkan shared_ptrparameter dengan nilai dan kemudian std::movedi dalam metode (seperti yang dilakukan dengan benar dalam kode Dentang), Anda memiliki:

  1. Saat memasukkan metode, shared_ptrparameter dibuat salinan: ref count atomic increment .
  2. Di dalam tubuh metode ini, Anda std::moveyang shared_ptrparameter ke dalam data anggota: count ref tidak tidak berubah! Anda hanya mencuri pointer / negara: tidak ada operasi penghitungan atom mahal.
  3. Saat keluar dari metode, shared_ptrparameter dirusak; tetapi karena Anda pindah di langkah 2, tidak ada yang dirusak, karena shared_ptrparameternya tidak menunjuk ke apa pun lagi. Sekali lagi, tidak ada penurunan atom yang terjadi dalam kasus ini.

Intinya: dalam hal ini Anda hanya mendapatkan satu kenaikan atom hitungan ref, yaitu hanya satu operasi atom .
Seperti yang Anda lihat, ini jauh lebih baik daripada dua peningkatan atom ditambah satu penurunan atom (total tiga operasi atom) untuk kasing.

Mr.C64
sumber
1
Juga perlu dicatat: mengapa mereka tidak melewati referensi const, dan menghindari seluruh std :: memindahkan barang? Karena pass-by-value juga memungkinkan Anda mengirimkan pointer mentah secara langsung dan hanya akan ada satu shared_ptr yang dibuat.
Joseph Ireland
@ JosephephIreland Karena Anda tidak dapat memindahkan referensi const
Bruno Ferreira
2
@ JosephephIreland karena jika Anda memanggilnya compilerInstance.setInvocation(std::move(sp));maka tidak akan ada kenaikan . Anda bisa mendapatkan perilaku yang sama dengan menambahkan kelebihan yang membutuhkan waktu shared_ptr<>&&tetapi mengapa duplikat ketika Anda tidak perlu.
ratchet freak
2
@ BrunoFerreira saya menjawab pertanyaan saya sendiri. Anda tidak perlu memindahkannya karena itu referensi, cukup salin saja. Masih hanya satu salinan, bukan dua. Alasan mereka tidak melakukan itu adalah karena itu tidak perlu menyalin shared_ptrs yang baru dibangun, misalnya dari setInvocation(new CompilerInvocation), atau seperti yang disebutkan ratchet setInvocation(std::move(sp)),. Maaf jika komentar pertama saya tidak jelas, saya sebenarnya mempostingnya secara tidak sengaja, sebelum saya selesai menulis, dan saya memutuskan untuk meninggalkannya
Joseph Ireland
22

Menyalin suatu shared_ptrmelibatkan menyalin pointer objek keadaan internal dan mengubah jumlah referensi. Memindahkannya hanya melibatkan penukaran pointer ke penghitung referensi internal, dan objek yang dimiliki, jadi lebih cepat.

SingerOfTheFall
sumber
16

Ada dua alasan untuk menggunakan std :: move dalam situasi ini. Sebagian besar tanggapan membahas masalah kecepatan, tetapi mengabaikan masalah penting dengan menunjukkan maksud kode dengan lebih jelas.

Untuk std :: shared_ptr, std :: move dengan jelas menunjukkan transfer kepemilikan pointee, sementara operasi penyalinan sederhana menambah pemilik tambahan. Tentu saja, jika pemilik asli kemudian melepaskan kepemilikan mereka (seperti dengan membiarkan std :: shared_ptr mereka dihancurkan), maka transfer kepemilikan telah dilakukan.

Ketika Anda mentransfer kepemilikan dengan std :: move, sudah jelas apa yang terjadi. Jika Anda menggunakan salinan normal, tidak jelas bahwa operasi yang dimaksud adalah transfer sampai Anda memverifikasi bahwa pemilik aslinya segera melepaskan kepemilikan. Sebagai bonus, implementasi yang lebih efisien adalah mungkin, karena transfer kepemilikan atom dapat menghindari keadaan sementara di mana jumlah pemilik telah meningkat satu (dan perubahan petugas dalam jumlah referensi).

Stephen C. Steel
sumber
Persis apa yang saya cari. Terkejut bagaimana jawaban lain mengabaikan perbedaan semantik yang penting ini. petunjuk cerdas adalah tentang kepemilikan.
qweruiop