Dlaczego miałbym std :: przenieść std :: shared_ptr?

148

Przeglądałem kod źródłowy Clang i znalazłem ten fragment:

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

Dlaczego miałbym chcieć ?std::movestd::shared_ptr

Czy jest jakiś sens przenoszenia własności do udostępnionego zasobu?

Dlaczego nie miałbym po prostu tego zrobić?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}
sdgfsdh
źródło

Odpowiedzi:

137

Myślę, że jedyną rzeczą, na którą inne odpowiedzi nie zostały wystarczająco podkreślone, jest kwestia szybkości .

std::shared_ptrliczba odniesień jest atomowa . zwiększenie lub zmniejszenie liczby referencyjnej wymaga atomowego przyrostu lub zmniejszenia . Jest to sto razy wolniejsze niż nieatomowe zwiększanie / zmniejszanie, nie wspominając o tym, że jeśli zwiększamy i zmniejszamy ten sam licznik, otrzymujemy dokładną liczbę, marnując przy tym mnóstwo czasu i zasobów.

Przesuwając shared_ptrzamiast kopiować, „kradniemy” atomową liczbę odniesień i unieważniamy drugą shared_ptr. „Kradzież” liczby referencyjnej nie jest atomowa i jest sto razy szybsza niż kopiowanie shared_ptr(i powodowanie atomowego zwiększania lub zmniejszania wartości referencyjnej).

Zwróć uwagę, że ta technika jest używana wyłącznie do optymalizacji. kopiowanie go (jak sugerowałeś) jest równie dobre pod względem funkcjonalności.

David Haim
źródło
5
Czy to naprawdę sto razy szybciej? Czy masz do tego punkty odniesienia?
xaviersjs
1
@xaviersjs Przypisanie wymaga atomowego przyrostu, po którym następuje atomowe zmniejszenie, gdy wartość wyjdzie poza zakres. Operacje atomowe mogą trwać setki cykli zegara. Więc tak, to naprawdę jest o wiele wolniejsze.
Adisak
2
@Adisak to pierwsza operacja pobierania i dodawania ( en.wikipedia.org/wiki/Fetch-and-add ) może zająć setki cykli więcej niż podstawowy przyrost. Czy masz do tego odniesienie?
xaviersjs
2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Przy operacjach na rejestrach, które składają się z kilku cykli, 100 (100-300) cykli atomowych pasuje do rachunku. Chociaż metryki pochodzą z 2013 r., Wydaje się, że jest to prawdą, zwłaszcza w przypadku systemów NUMA z wieloma gniazdami.
russianfool
1
Czasami myślisz, że nie ma wątków w twoim kodzie ... ale potem pojawia się jakaś cholerna biblioteka i psuje ją dla ciebie. Lepiej jest używać stałych referencji i std :: move ... jeśli jest jasne i oczywiste, że możesz ... niż polegać na liczbie odwołań do wskaźnika.
Erik Aronesty
123

Używając moveunikasz zwiększania, a następnie natychmiastowego zmniejszania liczby udziałów. To może zaoszczędzić ci drogich operacji atomowych na liczniku użycia.

Bo Persson
źródło
1
Czy nie jest to przedwczesna optymalizacja?
YSC
11
@YSC nie, jeśli ktokolwiek go tam umieścił, faktycznie go przetestował.
OrangeDog
19
@YSC Przedwczesna optymalizacja jest zła, jeśli sprawia, że ​​kod jest trudniejszy do odczytania lub utrzymania. Ten też nie ma, przynajmniej IMO.
Angew nie jest już dumny z
17
W rzeczy samej. To nie jest przedwczesna optymalizacja. Zamiast tego jest to rozsądny sposób zapisania tej funkcji.
Wyścigi lekkości na orbicie
60

Operacje przenoszenia (takie jak konstruktor przenoszenia) std::shared_ptrtanie , ponieważ w zasadzie są „wskaźnikami kradzieży” (ze źródła do celu; dokładniej mówiąc, cały blok kontroli stanu jest „skradziony” od źródła do celu, w tym informacje o liczbie referencyjnej) .

Zamiast tego operacje kopiowania przy std::shared_ptrwywołaniu niepodzielnego zwiększenia liczby referencji (tj. Nie tylko ++RefCountna składniku RefCountdanych całkowitych , ale np. Wywołanie InterlockedIncrementw systemie Windows) jest droższe niż zwykła kradzież wskaźników / stanu.

Tak więc, analizując szczegółowo dynamikę liczby ref w tym przypadku:

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

Jeśli przekażesz spwartość, a następnie weźmiesz kopię wewnątrz CompilerInstance::setInvocationmetody, masz:

  1. Przy wejściu do metody shared_ptrparametr jest konstruowany jako kopia: ref count atomowy przyrost .
  2. Wewnątrz ciała danej metody jest, to skopiuj ten shared_ptrparametr w elemencie danych: Ref liczyć atomową przyrost .
  3. Po wyjściu z metody shared_ptrparametr zostaje zniszczony: ref count atomic decment .

Masz dwa atomowe przyrosty i jeden atomowy dekrement, co daje w sumie trzy atomowe operacje.

Zamiast tego, jeśli przekażesz shared_ptrparametr według wartości, a następnie std::movewewnątrz metody (tak jak zostało to prawidłowo zrobione w kodzie Clanga), masz:

  1. Przy wejściu do metody shared_ptrparametr jest konstruowany jako kopia: ref count atomowy przyrost .
  2. W treści metody std::moveumieszczasz shared_ptrparametr w składniku danych: liczba ref nie zmienia się! Po prostu kradniesz wskaźniki / stan: nie są zaangażowane żadne kosztowne atomowe operacje liczenia referencji.
  3. Podczas wychodzenia z metody shared_ptrparametr zostaje zniszczony; ale ponieważ przeszedłeś w kroku 2, nie ma nic do zniszczenia, ponieważ shared_ptrparametr już na nic nie wskazuje. Ponownie, w tym przypadku nie dochodzi do dekrementacji atomowej.

Konkluzja: w tym przypadku otrzymujesz tylko jeden atomowy przyrost liczby ref, tj. Tylko jedną atomową operację.
Jak widać, jest to znacznie lepsze niż dwa atomowe przyrosty plus jeden atomowy dekrement (w sumie trzy atomowe operacje) dla wielkości kopii.

Panie C64
źródło
1
Warto również zauważyć: dlaczego po prostu nie przekazują przez stałe referencji i nie unikają całego std :: move? Ponieważ przekazywanie wartości pozwala również na bezpośrednie przekazywanie surowego wskaźnika i zostanie utworzony tylko jeden shared_ptr.
Joseph Ireland
@JosephIreland Ponieważ nie możesz przenieść odniesienia const
Bruno Ferreira
2
@JosephIreland, ponieważ jeśli nazwiesz to tak, compilerInstance.setInvocation(std::move(sp));nie będzie żadnego przyrostu . Możesz uzyskać to samo zachowanie, dodając przeciążenie, które zajmuje shared_ptr<>&&ale po co duplikować, gdy nie jest to konieczne.
ratchet freak
2
@BrunoFerreira Odpowiadałem na własne pytanie. Nie musisz go przenosić, ponieważ jest to odniesienie, po prostu go skopiuj. Wciąż tylko jeden egzemplarz zamiast dwóch. Powodem, dla którego tego nie robią, jest to, że niepotrzebnie skopiowałoby to nowo skonstruowane shared_ptrs, np. Z setInvocation(new CompilerInvocation)lub jak wspomniano w programie zapadkowym setInvocation(std::move(sp)). Przepraszam, jeśli mój pierwszy komentarz był niejasny, faktycznie opublikowałem go przez przypadek, zanim skończyłem pisać, i postanowiłem go po prostu zostawić
Joseph Ireland
22

Kopiowanie shared_ptrobejmuje kopiowanie wewnętrznego wskaźnika obiektu stanu i zmianę liczby odwołań. Przeniesienie obejmuje tylko zamianę wskaźników do wewnętrznego licznika odwołań i posiadanego obiektu, więc jest szybsze.

SingerOfTheFall
źródło
16

Istnieją dwa powody używania std :: move w tej sytuacji. Większość odpowiedzi dotyczyła kwestii szybkości, ale zignorowała ważną kwestię wyraźniejszego pokazania intencji kodu.

Dla std :: shared_ptr, std :: move jednoznacznie oznacza przeniesienie własności wskazanego, podczas gdy prosta operacja kopiowania dodaje dodatkowego właściciela. Oczywiście, jeśli pierwotny właściciel zrzeka się później swojej własności (na przykład pozwalając na zniszczenie ich std :: shared_ptr), wówczas przeniesienie własności zostało zrealizowane.

Kiedy przenosisz własność za pomocą std :: move, oczywiste jest, co się dzieje. Jeśli używasz zwykłej kopii, nie jest oczywiste, że zamierzoną operacją jest przeniesienie, dopóki nie zweryfikujesz, że pierwotny właściciel natychmiast zrzeka się prawa własności. Jako bonus możliwa jest wydajniejsza implementacja, ponieważ atomowe przeniesienie własności może uniknąć stanu tymczasowego, w którym liczba właścicieli wzrosła o jednego (i związane z tym zmiany w liczbie referencyjnej).

Stephen C. Steel
źródło
Dokładnie to, czego szukam. Zaskoczony, jak inne odpowiedzi ignorują tę ważną różnicę semantyczną. inteligentne wskazówki dotyczą własności.
qweruiop