Neden std :: bir std :: shared_ptr taşıyayım?


154

Clang kaynak koduna bakıyordum ve şu pasajı buldum:

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

Neden isteyeyim std::movebir std::shared_ptr?

Paylaşılan bir kaynakta sahipliği aktaran herhangi bir nokta var mı?

Neden bunun yerine bunu yapmayayım?

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

Yanıtlar:


151

Bence diğer yanıtların yeterince vurgulamadığı tek şey hız noktasıydı .

std::shared_ptrreferans sayısı atomiktir . referans sayısının artırılması veya azaltılması, atomik artış veya azalma gerektirir . Bu yüz katıdır yavaş daha olmayan atomik saymıyorum, artırma / eksiltme bu süreçte zaman ve kaynak bir ton israf, artırır ve biz tam sayısı ile rüzgar aynı sayaç azaltma eğer.

shared_ptrKopyalamak yerine hareket ettirerek atomik referans sayısını "çalarız" ve diğerini iptal ederiz shared_ptr. Referans sayısını "çalmak" atomik değildir ve kopyalamaktan shared_ptr(ve atomik referans artışına veya azalmasına neden olmaktan) yüz kat daha hızlıdır .

Bu tekniğin yalnızca optimizasyon için kullanıldığını unutmayın. kopyalamak (önerdiğiniz gibi) işlevsellik açısından da iyi.


7
Gerçekten yüz kat daha hızlı mı? Bunun için kriterleriniz var mı?
xaviersjs

1
@xaviersjs Atama, bir atomik artış ve ardından Değer kapsam dışına çıktığında bir atomik azalma gerektirir. Atomik işlemler yüzlerce saat döngüsü alabilir. Yani evet, gerçekten çok daha yavaş.
Adisak

2
@Adisak, getirme ve ekleme işlemini ilk duyduğum şeydir ( en.wikipedia.org/wiki/Fetch-and-add ), temel bir artıştan daha fazla yüzlerce döngü alabilir. Bunun için bir referansınız var mı?
xaviersjs

2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Kayıt işlemlerinin birkaç döngü olmasıyla, atomik için 100'lük (100-300) döngü tam olarak uyuyor. Ölçütler 2013 yılına ait olsa da, bu özellikle çok soketli NUMA sistemleri için hala geçerli gibi görünüyor.
russianfool

2
Bazen kodunuzda iş parçacığı olmadığını düşünürsünüz ... ama sonra bazı lanet kitaplıklar gelir ve onu sizin için mahveder. Sabit referansları kullanmak daha iyidir ve std :: move ... eğer yapabileceğin açık ve açıksa ... işaretçi referans sayılarına güvenmektense.
Erik Aronesty

127

Kullanarak move, hisse sayısını artırıp hemen düşürmekten kaçınırsınız. Bu, kullanım sayısında bazı pahalı atomik işlemlerden tasarruf etmenizi sağlayabilir.


2
Erken optimizasyon değil mi?
YSC

14
@YSC, oraya kim koyduysa gerçekten test etmediyse.
OrangeDog

21
@YSC Erken optimizasyon, kodu okumayı veya korumayı zorlaştırıyorsa kötüdür. Bu, en azından IMO'yu yapmıyor.
Angew artık

18
Aslında. Bu erken bir optimizasyon değildir. Bunun yerine, bu işlevi yazmanın mantıklı yolu budur.
Orbit'te Hafiflik Yarışları

65

Taşı (hareket yapıcısı gibi) işlemleri için std::shared_ptrolan ucuz temelde gibi, "çalarak işaretçiler" (kaynaktan bir hedefe, daha kesin konuşmak gerekirse, bütün durumu kontrol bloğu başvuru sayısı bilgileri de dahil olmak üzere, hedef kaynaktan "çalıntı" olduğu) .

Bunun yerine , atomik referans sayısı artışını çağırma üzerindeki kopyalama işlemleri (yani, yalnızca bir tamsayı veri üyesinde değil, örneğin Windows'da arama ), bu sadece işaretçileri / durumu çalmaktan daha pahalıdır .std::shared_ptr++RefCountRefCountInterlockedIncrement

Bu nedenle, bu vakanın ref sayısı dinamiklerini ayrıntılı olarak analiz etmek:

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

Eğer başarılı olursa spdeğeri tarafından ve daha sonra bir almak kopyasını içine CompilerInstance::setInvocationyöntemle, sahip:

  1. Yöntemi girerken, shared_ptrparametre kopyalanarak oluşturulur: ref count atomik artış .
  2. Yöntemin vücudun İçinde, kopyashared_ptr veri elemanı içerisine parametresini: saymak Ref atom artışı .
  3. Yöntemden çıkıldığında, shared_ptrparametre yok edilir: ref count atomik azalma .

Toplam üç atomik işlem için iki atomik artımınız ve bir atomik azalmanız var .

Bunun yerine, shared_ptrparametreyi değere göre geçirirseniz ve daha sonra std::moveyöntemin içinde (Clang kodunda doğru şekilde yapıldığı gibi), şunları elde edersiniz:

  1. Yöntemi girerken, shared_ptrparametre kopyalanarak oluşturulur: ref count atomik artış .
  2. Yöntemin vücudun İçinde, veri elemanı içerisine parametre: ref sayısı yok değil değiştirin! Sadece işaretçileri / durumu çalıyorsunuz: pahalı atomik referans sayma işlemleri dahil değildir.std::moveshared_ptr
  3. Yöntemden çıkılırken shared_ptrparametre yok edilir; ancak 2. adımda hareket ettiğinizden beri, shared_ptrparametre artık hiçbir şeye işaret etmediği için yok edilecek bir şey yok. Yine, bu durumda atomik azalma olmaz.

Alt satır: bu durumda sadece bir referans sayısı atomik artış, yani sadece bir atomik işlem elde edersiniz .
Gördüğünüz gibi, bu, kopya durumu için iki atomik artış artı bir atomik azalmadan (toplam üç atomik işlem için) çok daha iyidir .


1
Ayrıca kayda değer: neden sadece const referansıyla geçip tüm std :: move şeylerinden kaçınmıyorlar? Çünkü değere göre geçiş, ham bir işaretçiyi doğrudan iletmenize de izin verir ve yalnızca bir shared_ptr oluşturulur.
Joseph İrlanda

@JosephIreland Çünkü bir const referansını taşıyamazsınız
Bruno Ferreira

2
@JosephIreland çünkü onu böyle çağırırsanız artışcompilerInstance.setInvocation(std::move(sp)); olmayacak . Bir aşırı yükleme ekleyerek aynı davranışı elde edebilirsiniz, ancak gerekmediğinde neden yinelenir. shared_ptr<>&&
cırcır ucube

2
@BrunoFerreira Kendi sorumu cevaplıyordum. Referans olduğu için taşımanıza gerek yok, sadece kopyalayın. Hala iki yerine sadece bir kopya. Bunu yapmamalarının nedeni, yeni oluşturulan shared_ptr'leri gereksiz yere kopyalamalarıdır, örn. 'Den setInvocation(new CompilerInvocation)veya belirtildiği gibi setInvocation(std::move(sp)). İlk yorumum net değilse özür dilerim, yazmayı bitirmeden önce yanlışlıkla yayınladım ve bırakmaya karar verdim
Joseph Ireland

22

Bir shared_ptrkopyalamak, dahili durum nesnesi işaretçisini kopyalamayı ve referans sayısını değiştirmeyi içerir. Onu taşımak yalnızca işaretçileri dahili referans sayacına ve sahip olunan nesneye değiştirmeyi içerir, bu nedenle daha hızlıdır.


19

Bu durumda std :: move kullanmanın iki nedeni vardır. Yanıtların çoğu hız konusunu ele aldı, ancak kodun amacını daha net gösterme gibi önemli bir konuyu görmezden geldi.

Bir std :: shared_ptr için, std :: move açık bir şekilde noktanın sahipliğinin transferini belirtirken, basit bir kopyalama işlemi ek bir sahip ekler. Elbette, orijinal sahip daha sonra sahipliğini bırakırsa (örneğin, std :: shared_ptr'lerinin yok edilmesine izin vererek), o zaman bir mülkiyet devri gerçekleştirilmiştir.

Sahipliği std :: move ile aktardığınızda, ne olduğu açıktır. Normal bir kopya kullanırsanız, asıl sahibin sahiplikten derhal vazgeçtiğini doğrulayana kadar amaçlanan işlemin bir transfer olduğu açık değildir. Bonus olarak, daha verimli bir uygulama mümkündür, çünkü atomik bir sahiplik devri, sahiplerin sayısının bir arttığı geçici durumu (ve referans sayılarında görevlilerin değişmesini) önleyebilir.


Tam olarak aradığım şey. Diğer cevapların bu önemli anlamsal farkı nasıl görmezden geldiğine şaşırdım. akıllı işaretçiler tamamen sahiplikle ilgilidir.
qweruiop

Lambda gösteriminde sahipliğin özellikle önemli olduğunu düşünüyorum. Referans ile paylaşılan ptr yakalama, referans sayacına katkıda bulunmayabilir ve kod çıktıktan ve ptr yok edildikten sonra, sarkan işaretçi ile lambda'ya sahip olursunuz.
Vlad

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.