Чи існує неатомний еквівалент std :: shared_ptr? І чому в <memory> його немає?


88

Це дещо із двох частин питання про атомність std::shared_ptr:

1. Наскільки я можу зрозуміти, std::shared_ptrце єдиний розумний вказівник, <memory>який є атомним. Мені цікаво, чи є доступна неатомна версія std::shared_ptr(я не бачу нічого <memory>, тому я також відкритий для пропозицій за межами стандарту, таких як Boost). Я знаю, що boost::shared_ptrце також атомно (якщо BOOST_SP_DISABLE_THREADSне визначено), але, можливо, є інша альтернатива? Я шукаю те, що має таку саму семантику std::shared_ptr, що й не має атомності.

2. Я розумію, чому std::shared_ptrатомний; це якось приємно. Однак це не приємно для кожної ситуації, і в історії C ++ історично існувала мантра "платити лише за те, що ти використовуєш". Якщо я не використовую кілька потоків, або якщо я використовую декілька потоків, але не розподіляю право власності на вказівник між потоками, атомний розумний вказівник є надмірним. Моє друге запитання - чому std::shared_ptrв C ++ 11 не була надана неатомна версія ? (припускаючи, що є чому ) (якщо відповідь просто "неатомна версія просто ніколи не розглядалася" або "ніхто ніколи не просив неатомну версію", це добре!).

З питанням №2 мені цікаво, чи коли-небудь хтось пропонував неатомну версію shared_ptr(або Boost, або комітету стандартів) (не для заміни атомної версії shared_ptr, а для співіснування з нею), і вона була збита конкретна причина.


4
Яка "вартість" вас тут точно турбує? Вартість атомарного збільшення цілого числа? Це насправді вартість, яка стосується вас у будь-якому реальному додатку? Або ви просто передчасно оптимізуєте?
Nicol Bolas

9
@NicolBolas: Це більше цікавості, ніж будь-що інше; У мене (на даний момент) немає коду / проекту, де я серйозно хочу використовувати неатомний спільний вказівник. Однак у мене були проекти (раніше), де Boost's shared_ptrсуттєво сповільнювався через свою атомність, і визначення BOOST_DISABLE_THREADSдало помітну різницю (я не знаю, чи std::shared_ptrмали б ті самі витрати, що boost::shared_ptrбули).
Cornstalks

13
@Закрити виборців: яка частина питання не є конструктивною? Якщо немає конкретних причин, чому на друге запитання, це нормально (просте "це просто не враховувалось" було б достатньо вагомою відповіддю). Мені цікаво, чи існує конкретна причина / обгрунтування, яка існує. І перше питання, безумовно, є слушним, я б сказав. Якщо мені потрібно пояснити питання або внести до нього незначні корективи, будь ласка, дайте мені знати. Але я не бачу, як це не конструктивно.
Cornstalks

10
@Cornstalks Ну, мабуть, просто люди не так добре реагують на питання, які вони можуть легко відхилити як "передчасну оптимізацію" , якими б ясними не були обгрунтовані, правильно поставлені чи відповідні. Я для себе не бачу жодної причини закривати це як неконструктивне.
Крістіан Рау

13
(не можу написати відповідь зараз, вона закрита, тому коментуючи) З GCC, коли ваша програма не використовує декілька потоків shared_ptr, не використовує атомні операційні системи для перерахунку. Див. (2) за адресою gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html для виправлення для GCC, щоб дозволити неатомну реалізацію використовувати навіть у багатопотокових програмах для shared_ptrоб’єктів, якими спільно не користуються нитки. Я сидів на цьому патчі роками, але я розглядаю можливість нарешті здійснити його для GCC 4.9
Джонатан Уейклі

Відповіді:


105

1. Мені цікаво, чи існує неатомна версія std :: shared_ptr

Не передбачено стандартом. Цілком може бути такий, що надається бібліотекою "сторонніх розробників". Дійсно, до С ++ 11 і до Boost здавалося, що кожен писав свій власний розумний вказівник з урахуванням посилань (включаючи мене самого).

2. Моє друге запитання - чому в C ++ 11 не була надана неатомна версія std :: shared_ptr?

Це питання обговорювалося на засіданні Рапперсвіля в 2010 році. Тема була представлена ​​коментарем Національного органу № 20 Швейцарії. З обох сторін дискусії існували вагомі аргументи, включаючи ті, які ви наводите у своєму питанні. Однак наприкінці обговорення голосування було переважним (але не одностайним) проти додавання несинхронізованої (неатомної) версії shared_ptr.

Аргументи проти включені:

  • Код, написаний за допомогою несинхронізованого shared_ptr, може в кінцевому підсумку використовуватись у різьбовому коді по дорозі, що призведе до складних проблем з налагодженням без попередження.

  • Наявність одного "універсального" shared_ptr, який є "одним із шляхів" для трафіку при підрахунку посилань, має переваги: ​​З оригінальної пропозиції :

    Має однаковий тип об’єкта незалежно від використовуваних функцій, що значно полегшує взаємодію між бібліотеками, включаючи сторонні бібліотеки.

  • Вартість атомів, хоча і не дорівнює нулю, не є надзвичайною. Витрати пом'якшуються використанням конструкції переміщення та призначення переміщення, для яких не потрібно використовувати атомні операції. Такі операції зазвичай використовуються для vector<shared_ptr<T>>стирання та вставки.

  • Ніщо не забороняє людям писати власний розумний вказівник, що не враховується атомними посиланнями, якщо це насправді вони хочуть зробити.

Останнім словом від LWG у Рапперсвілі того дня було:

Відхилити СН 20. На даний момент немає консенсусу щодо внесення змін.


7
Ого, ідеально, дякую за інформацію! Саме таку інформацію я сподівався знайти.
Cornstalks

> Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. це надзвичайно дивні міркування. Сторонні бібліотеки в будь-якому випадку надаватимуть власні типи, то чому б це мало значення, якщо вони надавали їх у формі std :: shared_ptr <CustomType>, std :: non_atomic_shared_ptr <CustomType> тощо? Вам завжди доведеться пристосовувати свій код до того, що бібліотека повертає,
Жан-Міхаел Сельє

Що стосується конкретних типів бібліотек, це правда, але ідея полягає в тому, що в сторонніх API є також багато місць, де стандартні типи відображаються у сторонніх API. Наприклад, моя бібліотека може std::shared_ptr<std::string>десь взяти . Якщо чужа бібліотека теж приймає цей тип, абоненти можуть передавати нам одні й ті самі рядки без незручностей або накладних витрат при конвертації між різними уявленнями, і це невеликий виграш для всіх.
Джек О'Коннор,

52

Говард вже добре відповів на питання, і Ніколь зробив кілька хороших зауважень щодо переваг наявності єдиного стандартного спільного типу покажчика, а не безлічі несумісних.

Хоча я повністю погоджуюся з рішенням комітету, я думаю, що є певна користь від використання несинхронізованого shared_ptrтипу в особливих випадках , тому я досліджував тему кілька разів.

Якщо я не використовую кілька потоків, або якщо я використовую декілька потоків, але не розподіляю право власності на вказівник між потоками, атомний розумний вказівник є надмірним.

З GCC, коли ваша програма не використовує кілька потоків, shared_ptr не використовує атомарні операційні системи для перерахунку. Це робиться шляхом оновлення лічильників посилань за допомогою функцій обгортки, які визначають, чи є програма багатопотоковою (на GNU / Linux це робиться просто виявляючи, чи програма посилається на libpthread.so), і направляючи відповідно до атомних або неатомних операцій.

Багато років тому я зрозумів, що оскільки GCC shared_ptr<T>реалізовано з точки зору __shared_ptr<T, _LockPolicy>базового класу , можна використовувати базовий клас з однопотоковою політикою блокування навіть у багатопотоковому коді, явно використовуючи __shared_ptr<T, __gnu_cxx::_S_single>. На жаль, оскільки це не було передбачуваним випадком використання, він не працював оптимально до GCC 4.9, а деякі операції все ще використовували функції обгортки і тому надсилалися до атомних операцій, навіть якщо ви явно запитували _S_singleполітику. Див. Пункт (2) на http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.htmlдля отримання детальнішої інформації та виправлення для GCC, щоб дозволити неатомну реалізацію використовувати навіть у багатопотокових програмах. Я сидів на цьому патчі роками, але нарешті завершив його для GCC 4.9, що дозволяє використовувати шаблон псевдоніма, як цей, для визначення спільного типу вказівника, який не є безпечним для потоку, але трохи швидший:

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

Цей тип не буде взаємодіяти з ним std::shared_ptr<T>і буде безпечним у використанні лише тоді, коли гарантується, що shared_ptr_unsynchronizedоб’єкти ніколи не будуть спільно використовуватися між потоками без додаткової синхронізації, яку надає користувач.

Це, звичайно, абсолютно не портативно, але іноді це нормально. За умови правильного злому препроцесора ваш код все одно буде добре працювати з іншими реалізаціями, якщо shared_ptr_unsynchronized<T>це псевдонім, для shared_ptr<T>GCC це буде просто трохи швидше.


Якщо ви використовуєте GCC до версії 4.9, ви можете скористатися цим, додавши _Sp_counted_base<_S_single>явні спеціалізації до власного коду (і забезпечивши, щоб ніхто ніколи __shared_ptr<T, _S_single>не створював екземпляри, не включаючи спеціалізації, щоб уникнути порушень ОДР.) Додавання таких спеціалізацій stdтипів технічно не визначено, але працювати на практиці, тому що в цьому випадку немає ніякої різниці між тим, як я додаю спеціалізації до GCC, або тим, що додаю їх до свого коду.


2
Просто цікаво, чи є у вашому прикладі псевдонім шаблону помилка? Тобто я думаю, що це повинно читати shared_ptr_unsynchronized = std :: __ shared_ptr <. До речі, я перевірив це сьогодні, спільно зі std :: __ enable_shared_from_this та std :: __слабкий_ппр, і, здається, він працює добре (gcc 4.9 та gcc 5.2). Я швидко його розберу / розберу, щоб перевірити, чи дійсно атомні операції пропущені.
Carl Cook

Чудові деталі! Нещодавно я зіткнувся питання, як описано в цьому питанні , що в кінцевому підсумку змусило мене заглянути в вихідний код std::shared_ptr, std::__shared_ptr, __default_lock_policyі тому подібне. Ця відповідь підтвердила те, що я зрозумів із коду.
Nawaz

21

Моє друге запитання - чому в C ++ 11 не була надана неатомна версія std :: shared_ptr? (припускаючи, що є чому).

Можна так само легко запитати, чому немає нав'язливого вказівника, чи будь-якої кількості інших можливих варіацій спільних покажчиків, які можна було б мати.

Дизайн shared_ptr, переданий від Boost, полягав у створенні мінімальної стандартної лінгва-франки розумних покажчиків. Що, загалом кажучи, ви можете просто зняти це зі стіни і використати. Це те, що можна було б використовувати загалом для широкого спектру застосувань. Ви можете помістити його в інтерфейс, і, мабуть, хороші люди будуть готові ним скористатися.

Потоки потоків лише стануть більш поширеними в майбутньому. Справді, з часом різьблення, як правило, є одним з основних засобів досягнення продуктивності. Потребування базового розумного вказівника для виконання мінімального рівня, необхідного для підтримки різьбових потоків, сприяє цій реальності.

Закидати півдюжини розумних покажчиків з незначними варіаціями між ними у стандартний, або навіть гірше розумний вказівник на основі політики, було б жахливо. Кожен вибирав би вказівник, який йому найбільше подобається, і відмовлявся від усіх інших. Ніхто не зміг би спілкуватися ні з ким іншим. Це було б подібно до поточної ситуації зі рядками C ++, коли кожен має свій тип. Тільки набагато гірше, тому що взаємодія з рядками набагато легше, ніж взаємодія між класами розумних покажчиків.

Boost, і, за розширенням комітет, обрав конкретний розумний вказівник для використання. Він забезпечував хороший баланс функцій і широко застосовувався на практиці.

std::vectorу деяких кутових випадках також має деякі неефективності порівняно з оголеними масивами. Він має деякі обмеження; деякі способи використання дійсно хочуть жорстко обмежувати розмір a vector, не використовуючи метальний розподільник. Однак комітет не задумував, vectorщоб бути всім для всіх. Він був розроблений, щоб бути гарним за замовчуванням для більшості програм. Ті, для кого це не може працювати, можуть просто написати альтернативу, яка відповідає їхнім потребам.

Як і для розумного вказівника, якщо shared_ptrатомність є тягарем. Знову ж таки, можна також подумати, щоб не копіювати їх стільки.


7
+1 за "можна також подумати, щоб не копіювати їх стільки".
Алі

Якщо ви коли-небудь підключили профайлер, ви особливий, і ви можете просто налаштувати такі аргументи, як вище. Якщо у вас немає операційних вимог, які важко задовольнити, не слід використовувати C ++. Сперечаючись з вами, це хороший спосіб зробити C ++ загальноприйнятим для всіх, хто зацікавлений у високій продуктивності або низькій затримці. Ось чому програмісти ігор не використовують STL, boost або навіть винятки.
Ганс Мальербе

Для ясності, я думаю , що котирування в верхній частині вашої відповіді слід читати «почему не неатоміческая версія станд :: shared_ptr представлена в C ++ 11?»
Шарль Савойя

4

Я готую доповідь на shared_ptr на роботі. Я використовую модифікований boost shared_ptr, щоб уникнути окремого malloc (наприклад, що може зробити make_shared) та параметр шаблону для політики блокування, як shared_ptr_unsynchronized, згаданий вище. Я використовую програму з

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

як тест, після очищення непотрібних копій shared_ptr. Програма використовує лише основний потік, і відображається аргумент тесту. Тест env - це ноутбук, на якому запущено linuxmint 14. Ось час, зайнятий у секундах:

тестовий запуск налаштування boost (1,49) std із зміненим boost make_shared
mt-небезпечно (11) 11,9 9 / 11,5 (-пряжа на) 8.4  
атомна (11) 13,6 12,4 13,0  
mt-небезпечно (12) 113,5 85,8 / 108,9 (-під ниткою) 81,5  
атомна (12) 126,0 109,1 123,6  

Тільки версія 'std' використовує -std = cxx11, а -pthread, ймовірно, перемикає lock_policy у класі g ++ __shared_ptr.

З цих цифр я бачу вплив атомних інструкцій на оптимізацію коду. Тестовий приклад не використовує контейнери C ++, але vector<shared_ptr<some_small_POD>>, ймовірно, постраждає, якщо об’єкт не потребує захисту потоку. Boost страждає менш імовірно, оскільки додатковий malloc обмежує кількість вкладених файлів та оптимізацію коду.

Я ще не знайшов машину з достатньою кількістю ядер для стрес-тесту на масштабованість атомних інструкцій, але використання std :: shared_ptr лише тоді, коли це необхідно, напевно, краще.


3

Посилення забезпечує shared_ptrнеатомну. Це називається local_shared_ptr, і його можна знайти в бібліотеці розумних покажчиків boost.


+1 для короткої твердої відповіді з хорошим цитуванням, але цей тип покажчика виглядає дорого - як з точки зору пам'яті, так і часу роботи, завдяки одному додатковому рівню опосередкованості (local-> shared-> ptr vs shared-> ptr).
Red. Хвиля

@ Red.Wave Чи можете ви пояснити, що ви маєте на увазі під непрямим впливом, і як це впливає на ефективність? Ви маєте на увазі, що це так чи shared_ptrінакше з лічильником, хоча воно місцеве? Або ви маєте на увазі, що в цьому є інша проблема? Документи кажуть, що різниця лише в тому, що це не атомно.
Квантовий фізик

Кожен локальний ptr зберігає підрахунок і посилання на вихідний спільний ptr. Таким чином, будь-який доступ до остаточного пуанте потребує відхилення від місцевого до спільного ptr, який потім є відхиленням від пуанте. Таким чином, є ще одна непряма укладена до непрямих з спільних ptr. І це збільшує накладні витрати.
Ред. Хвиля

@ Red.Wave Звідки ти береш цю інформацію? Це: "Отже, будь-який доступ до остаточного пуанте потребує відхилення від місцевого до спільного ptr". Я не зміг цього знайти в документах для посилення. Знову - таки, що я бачив в документації, що він говорить , що local_shared_ptrі shared_ptrє ідентичними , за винятком атомної. Мені щиро цікаво з’ясувати, чи відповідає те, що ви говорите, тому, що я використовую local_shared_ptrв додатках, які вимагають високої продуктивності.
Квантовий фізик

3
@ Red.Wave Якщо ви подивитесь на фактичний вихідний код github.com/boostorg/smart_ptr/blob/…, то побачите, що подвійного опосередкування немає. Цей параграф у документації - це лише розумова модель.
Ілля Попов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.