Czy istnieje nieatomowy odpowiednik std :: shared_ptr? A dlaczego takiego nie ma w <memory>?

88

To jest trochę dwuczęściowe pytanie, dotyczące atomowości std::shared_ptr:

1. O ile wiem, std::shared_ptrjest to jedyny inteligentny wskaźnik w <memory>tym atomowym. Zastanawiam się, czy jest std::shared_ptrdostępna nieatomowa wersja programu (w środku nic nie widzę <memory>, więc jestem również otwarty na sugestie spoza standardu, jak te w Boost). Wiem, że boost::shared_ptrjest również atomowy (jeśli BOOST_SP_DISABLE_THREADSnie jest zdefiniowany), ale może jest inna alternatywa? Szukam czegoś, co ma taką samą semantykę jak std::shared_ptr, ale bez atomowości.

2. Rozumiem, dlaczego std::shared_ptrjest atomowy; to całkiem miłe. Jednak nie jest to dobre rozwiązanie w każdej sytuacji, a C ++ zawsze wyznawał mantrę „płać tylko za to, czego używasz”. Jeśli nie używam wielu wątków lub jeśli używam wielu wątków, ale nie udostępniam własności wskaźnika między wątkami, atomowy inteligentny wskaźnik jest przesadą. Moje drugie pytanie brzmi: dlaczego nie atomowa wersja nie została std::shared_ptrudostępniona w C ++ 11 ? (zakładając, że istnieje przyczyna ) (jeśli odpowiedź brzmi po prostu „wersja nieatomowa nigdy nie była brana pod uwagę” lub „nikt nigdy nie prosił o wersję nieatomową” to w porządku!).

Odnośnie pytania nr 2, zastanawiam się, czy ktoś kiedykolwiek zaproponował nieatomową wersję shared_ptr(do Boost lub komisji normalizacyjnej) (nie po to, aby zastąpić atomową wersję shared_ptr, ale aby z nią współistnieć) i została zestrzelona specyficzny powód.

Łodygi kukurydzy
źródło
4
O jaki „koszt” dokładnie się martwisz? Koszt atomowego zwiększania liczby całkowitej? Czy to rzeczywiście koszt, który Cię niepokoi w przypadku każdego rzeczywistego zastosowania? A może przedwcześnie optymalizujesz?
Nicol Bolas
9
@NicolBolas: To bardziej zaciekawienie niż cokolwiek innego; Nie mam (obecnie) żadnego kodu / projektu, w którym poważnie chciałbym użyć nieatomowego wskaźnika współdzielonego. Jednak miałem projekty (w przeszłości), w których Boost's shared_ptrbył znacznym spowolnieniem ze względu na jego atomowość, a zdefiniowanie BOOST_DISABLE_THREADSspowodowało zauważalną różnicę (nie wiem, czy std::shared_ptrmiałoby ten sam koszt, co boost::shared_ptrwcześniej).
Cornstalks
13
@Zamknij wyborcy: która część pytania nie jest konstruktywna? Jeśli nie ma specyficzny dlaczego do drugiego pytania, to w porządku (proste „to po prostu nie był uważany za” byłaby ważna odpowiedź wystarczy). Jestem ciekawy, czy istnieje konkretny powód / uzasadnienie, które istnieje. Powiedziałbym, że pierwsze pytanie jest z pewnością ważne. Jeśli muszę wyjaśnić pytanie lub wprowadzić niewielkie poprawki, daj mi znać. Ale nie rozumiem, jak to nie jest konstruktywne.
Cornstalks
10
@Cornstalks Cóż, prawdopodobnie po prostu ludzie nie reagują tak dobrze na pytania, które można łatwo odrzucić jako „przedwczesną optymalizację” , bez względu na to, jak ważne, dobrze postawione lub istotne jest to pytanie. Osobiście nie widzę powodu, aby zamknąć to jako niekonstruktywne.
Christian Rau
13
(nie można napisać odpowiedzi, teraz jest ona zamknięta, więc komentowanie) Z GCC, gdy twój program nie używa wielu wątków shared_ptr, nie używa atomic ops do refcount. Zobacz (2) na gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html, aby zapoznać się z poprawką do GCC, która umożliwia użycie nieatomowej implementacji nawet w aplikacjach wielowątkowych dla shared_ptrobiektów, które nie są współużytkowane między wątki. Siedzę nad tą łatką od lat, ale rozważam ostateczne wydanie jej na GCC 4.9
Jonathan Wakely

Odpowiedzi:

105

1. Zastanawiam się, czy jest dostępna nie-atomowa wersja std :: shared_ptr

Niedostępne w standardzie. Może być udostępniony przez bibliotekę „innej firmy”. Rzeczywiście, przed C ++ 11 i przed Boost, wydawało się, że każdy pisał własne referencje liczone inteligentnym wskaźnikiem (łącznie ze mną).

2. Moje drugie pytanie brzmi: dlaczego nie atomowa wersja std :: shared_ptr nie została dostarczona w C ++ 11?

Kwestia ta była omawiana na spotkaniu w Rapperswil w 2010 roku. Temat został wprowadzony w komentarzu nr 20 organu krajowego Szwajcarii. Po obu stronach debaty pojawiły się silne argumenty, w tym te, które przedstawiłeś w swoim pytaniu. Jednak pod koniec dyskusji głosowanie było przeważające (ale nie jednomyślne) przeciwko dodaniu niezsynchronizowanej (nie atomowej) wersji shared_ptr.

Zawiera argumenty przeciwko:

  • Kod napisany za pomocą niezsynchronizowanego shared_ptr może w końcu zostać użyty w kodzie podzielonym na wątki, powodując trudne do debugowania problemy bez ostrzeżenia.

  • Posiadanie jednego „uniwersalnego” shared_ptr, który jest „jedyną drogą” do ruchu w zliczaniu referencji, ma zalety: Z pierwotnej oferty pakietowej :

    Ma ten sam typ obiektu niezależnie od używanych funkcji, co znacznie ułatwia współdziałanie między bibliotekami, w tym bibliotekami innych firm.

  • Koszt atomów, choć nie jest zerowy, nie jest przytłaczający. Koszt jest zmniejszony przez zastosowanie konstrukcji ruchu i przypisania ruchu, które nie wymagają użycia operacji atomowych. Takie operacje są powszechnie używane w vector<shared_ptr<T>>usuwaniu i wstawianiu.

  • Nic nie stoi na przeszkodzie, aby ludzie pisali własny, nieatomowy wskaźnik liczony jako odniesienie, jeśli naprawdę tego chcą.

Ostatnie słowo LWG w Rapperswilu tego dnia brzmiało:

Odrzuć CH 20. Brak zgody na wprowadzenie zmian w tej chwili.

Howard Hinnant
źródło
7
Wow, doskonale, dzięki za informację! Dokładnie takie informacje chciałem znaleźć.
Cornstalks
> Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. to bardzo dziwne rozumowanie. Biblioteki stron trzecich i tak dostarczą własne typy, więc dlaczego miałoby to mieć znaczenie, gdyby udostępniły je w postaci std :: shared_ptr <CustomType>, std :: non_atomic_shared_ptr <CustomType> itp.? zawsze będziesz musiał dostosować swój kod do tego, co i tak zwraca biblioteka
Jean-Michaël Celerier
To prawda, jeśli chodzi o typy specyficzne dla biblioteki, ale chodzi o to, że istnieje również wiele miejsc, w których standardowe typy pojawiają się w interfejsach API innych firm. Na przykład moja biblioteka może std::shared_ptr<std::string>gdzieś zająć . Jeśli biblioteka innej osoby również przyjmuje ten typ, wywołujący mogą przekazywać te same ciągi do nas obu bez niedogodności lub narzutów związanych z konwersją między różnymi reprezentacjami, a to mała korzyść dla każdego.
Jack O'Connor
52

Howard już dobrze odpowiedział na to pytanie, a Nicol przedstawił kilka dobrych uwag na temat korzyści wynikających z posiadania jednego standardowego typu współdzielonego wskaźnika, zamiast wielu niekompatybilnych.

Chociaż całkowicie zgadzam się z decyzją komisji, uważam, że stosowanie w szczególnych przypadkach niezsynchronizowanego shared_ptrtypu podobnego do tego przynosi pewne korzyści , więc zbadałem ten temat kilka razy.

Jeśli nie używam wielu wątków lub jeśli używam wielu wątków, ale nie udostępniam własności wskaźnika między wątkami, atomowy inteligentny wskaźnik jest przesadą.

Z GCC, gdy twój program nie używa wielu wątków shared_ptr nie używa atomic ops do refcount. Odbywa się to poprzez aktualizację liczników odwołań za pomocą funkcji opakowujących, które wykrywają, czy program jest wielowątkowy (w systemie GNU / Linux odbywa się to po prostu przez wykrycie, czy program jest powiązany z libpthread.so) i odpowiednio wysyłają do operacji atomowych lub nieatomowych.

Wiele lat temu zdałem sobie sprawę, że ponieważ GCC shared_ptr<T>jest implementowane w postaci __shared_ptr<T, _LockPolicy>klasy bazowej , możliwe jest użycie klasy bazowej z jednowątkową polityką blokowania nawet w kodzie wielowątkowym, poprzez jawne użycie __shared_ptr<T, __gnu_cxx::_S_single>. Niestety, ponieważ nie był to zamierzony przypadek użycia, nie działał optymalnie przed GCC 4.9, a niektóre operacje nadal korzystały z funkcji opakowujących i wysyłane do operacji atomowych, mimo że wyraźnie zażądałeś tej _S_singlepolityki. Patrz punkt (2) na http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.htmlaby uzyskać więcej szczegółów i łatkę do GCC, aby umożliwić stosowanie nieatomowej implementacji nawet w aplikacjach wielowątkowych. Siedziałem nad tą poprawką przez lata, ale w końcu zdecydowałem się na to dla GCC 4.9, które pozwala ci użyć szablonu aliasu, takiego jak ten, do zdefiniowania typu współdzielonego wskaźnika, który nie jest bezpieczny dla wątków, ale jest nieco szybszy:

template<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

Ten typ nie byłby interoperacyjny std::shared_ptr<T>i byłby bezpieczny w użyciu tylko wtedy, gdy jest zagwarantowane, że shared_ptr_unsynchronizedobiekty nigdy nie będą współużytkowane między wątkami bez dodatkowej synchronizacji dostarczonej przez użytkownika.

Jest to oczywiście całkowicie nieprzenośne, ale czasami jest to w porządku. Z właściwymi hackami preprocesora, twój kod nadal działałby dobrze z innymi implementacjami, gdyby shared_ptr_unsynchronized<T>był aliasem dla shared_ptr<T>, byłby trochę szybszy z GCC.


Jeśli używasz GCC w wersji wcześniejszej niż 4.9, możesz to wykorzystać, dodając _Sp_counted_base<_S_single>jawne specjalizacje do własnego kodu (i upewniając się, że nikt nigdy nie tworzy instancji __shared_ptr<T, _S_single>bez uwzględnienia specjalizacji, aby uniknąć naruszeń ODR). Dodanie takich specjalizacji stdtypów jest technicznie niezdefiniowane, ale byłoby w praktyce, bo w tym przypadku nie ma różnicy między mną dodaniem specjalizacji do GCC, a Tobą dodaniem ich do własnego kodu.

Jonathan Wakely
źródło
2
Zastanawiam się tylko, czy w Twoim przykładzie aliasu szablonu jest literówka? To znaczy myślę, że powinien przeczytać shared_ptr_unsynchronized = std :: __ shared_ptr <. Nawiasem mówiąc, przetestowałem to dzisiaj, w połączeniu z std :: __ enable_shared_from_this i std :: __ słaby_ptr i wygląda na to, że działa dobrze (gcc 4.9 i gcc 5.2). Wkrótce sprofiluję / zdemontuję to, aby zobaczyć, czy rzeczywiście operacje atomowe są pomijane.
Carl Cook
Niesamowite szczegóły! Niedawno w obliczu problemu, jak opisano w tej kwestii , że w końcu mnie zajrzeć do kodu źródłowego std::shared_ptr, std::__shared_ptr, __default_lock_policyi takie. Ta odpowiedź potwierdziła to, co zrozumiałem z kodu.
Nawaz
21

Moje drugie pytanie brzmi: dlaczego nie atomowa wersja std :: shared_ptr nie została dostarczona w C ++ 11? (zakładając, że istnieje dlaczego).

Równie łatwo można zapytać, dlaczego nie ma natrętnego wskaźnika lub dowolnej liczby innych możliwych odmian wspólnych wskaźników, jakie można mieć.

Projekt shared_ptr, przekazany przez Boost, miał na celu stworzenie minimalnego standardu lingua-franca inteligentnych wskaźników. To, ogólnie rzecz biorąc, możesz po prostu zdjąć to ze ściany i użyć. Jest to coś, co byłoby powszechnie używane w wielu różnych zastosowaniach. Możesz umieścić go w interfejsie i są duże szanse, że ludzie będą chcieli go używać.

W przyszłości wątkowanie stanie się bardziej powszechne. Rzeczywiście, w miarę upływu czasu wątki będą na ogół jednym z głównych sposobów osiągnięcia wydajności. Wymóg, aby podstawowy inteligentny wskaźnik wykonywał absolutne minimum potrzebne do obsługi wątków, ułatwia tę rzeczywistość.

Wrzucenie do standardu pół tuzina inteligentnych wskaźników z niewielkimi różnicami między nimi lub, co gorsza, inteligentnego wskaźnika opartego na polityce, byłoby straszne. Każdy wybrałby wskazówkę, którą najbardziej lubił, i wyrzekł się wszystkich innych. Nikt nie byłby w stanie komunikować się z nikim innym. Byłoby to tak, jak obecne sytuacje z napisami C ++, gdzie każdy ma swój własny typ. Tylko o wiele gorsze, ponieważ współdziałanie z łańcuchami jest o wiele łatwiejsze niż współdziałanie między klasami inteligentnych wskaźników.

Boost, a co za tym idzie, komitet wybrał konkretny inteligentny wskaźnik do użycia. Zapewniała dobrą równowagę funkcji i była szeroko i powszechnie stosowana w praktyce.

std::vectorma pewne nieefektywności w porównaniu z gołymi tablicami w niektórych przypadkach narożnych. Ma pewne ograniczenia; niektóre zastosowania naprawdę chcą mieć sztywny limit rozmiaru a vector, bez używania alokatora rzucającego. Jednak komitet nie zaprojektował tak, vectoraby być wszystkim dla wszystkich. Został zaprojektowany tak, aby był dobrym domyślnym dla większości aplikacji. Ci, dla których to nie zadziała, mogą po prostu napisać alternatywę, która odpowiada ich potrzebom.

Tak jak dla inteligentnego wskaźnika, jeśli shared_ptratomowość jest ciężarem. Z drugiej strony można by też rozważyć nie kopiowanie ich tak często.

Nicol Bolas
źródło
7
+1 dla „można też rozważyć nie kopiowanie ich tak często”.
Ali
Jeśli kiedykolwiek podłączysz profilera, jesteś wyjątkowy i możesz po prostu wyłączyć argumenty, takie jak powyższe. Jeśli nie masz wymagań operacyjnych, które są trudne do spełnienia, nie powinieneś używać języka C ++. Twierdzenie, jak to robisz, jest dobrym sposobem, aby C ++ był powszechnie krytykowany przez każdego, kogo interesuje wysoka wydajność lub małe opóźnienia, dlatego programiści gier nie używają STL, boost ani nawet wyjątków.
Hans Malherbe
Dla jasności myślę, że cytat u góry odpowiedzi powinien brzmieć „dlaczego nie atomowa wersja std :: shared_ptr nie została dostarczona w C ++ 11?”
Charles Savoie
4

Przygotowuję referat na temat shared_ptr w pracy. Używałem zmodyfikowanego boost shared_ptr z unikaniem oddzielnego malloc (jak to, co może zrobić make_shared) i parametru szablonu dla zasad blokowania, takich jak shared_ptr_unsynchronized, o którym mowa powyżej. Używam programu z

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

jako test, po wyczyszczeniu niepotrzebnych kopii shared_ptr. Program używa tylko głównego wątku i wyświetlany jest argument testowy. Środowisko testowe to komputer przenośny z systemem LinuxMint 14. Oto czas w sekundach:

test run setup boost (1.49) std ze zmodyfikowanym boostem make_shared
mt-unsafe (11) 11,9 9 / 11,5 (-pthread on) 8.4  
atomowy (11) 13,6 12,4 13,0  
mt-unsafe (12) 113,5 85,8 / 108,9 (-pthread on) 81,5  
atomowy (12) 126,0 109,1 123,6  

Tylko wersja „std” używa -std = cxx11, a opcja -pthread prawdopodobnie przełącza lock_policy w klasie g ++ __shared_ptr.

Na podstawie tych liczb widzę wpływ atomowych instrukcji na optymalizację kodu. Przypadek testowy nie używa żadnych kontenerów C ++, ale vector<shared_ptr<some_small_POD>>prawdopodobnie ucierpi, jeśli obiekt nie potrzebuje ochrony wątku. Boost cierpi mniej prawdopodobnie, ponieważ dodatkowy malloc ogranicza ilość wstawiania i optymalizacji kodu.

Nie znalazłem jeszcze maszyny z wystarczającą liczbą rdzeni, aby przetestować skalowalność instrukcji atomowych, ale użycie std :: shared_ptr tylko wtedy, gdy jest to konieczne, jest prawdopodobnie lepsze.

Russ
źródło
3

Boost zapewnia to shared_ptr, co nie jest atomowe. Nazywa się local_shared_ptri można go znaleźć w bibliotece inteligentnych wskaźników doładowania.

Fizyk kwantowy
źródło
+1 dla krótkiej, solidnej odpowiedzi z dobrym cytowaniem, ale ten typ wskaźnika wygląda kosztownie - zarówno pod względem pamięci, jak i czasu wykonania, ze względu na jeden dodatkowy poziom pośredni (local-> shared-> ptr vs shared-> ptr).
Red.Wave
@ Red.Wave Czy możesz wyjaśnić, co masz na myśli mówiąc o pośrednictwie i jak wpływa to na wydajność? Czy masz na myśli, że i tak jest shared_ptrz licznikiem, mimo że jest lokalny? A może masz na myśli inny problem? Doktorzy mówią, że jedyną różnicą jest to, że to nie jest atomowe.
Fizyk kwantowy
Każdy lokalny ptr przechowuje liczbę i odniesienie do oryginalnego udostępnionego ptr. Tak więc każdy dostęp do końcowego pointee wymaga wyprowadzenia z lokalnego na współdzielony ptr, który jest następnie wyrefundowany dla wskazanego. W ten sposób istnieje jeszcze jeden kierunek pośredni powiązany z pośrednimi współdzielonymi ptr. A to zwiększa koszty ogólne.
Red.Wave
@ Red.Wave Skąd czerpiesz te informacje? To: „Tak więc każdy dostęp do końcowego pointee wymaga wyodrębnienia z lokalnego do współdzielonego ptr” wymaga pewnego cytowania. Nie mogłem znaleźć tego w dokumentach wspomagających. Ponownie, to, co zobaczyłem w dokumentach, to to, że mówi to local_shared_ptri shared_ptrsą identyczne z wyjątkiem atomowego. Naprawdę jestem zainteresowany, aby dowiedzieć się, czy to, co mówisz, jest prawdą, ponieważ używam local_shared_ptrw aplikacjach wymagających wysokiej wydajności.
Fizyk kwantowy
3
@ Red.Wave Jeśli spojrzysz na rzeczywisty kod źródłowy github.com/boostorg/smart_ptr/blob/ ... zobaczysz, że nie ma podwójnego pośrednictwa. Ten akapit w dokumentacji to tylko model myślowy.
Ilya Popov