Чому б я std :: перемістити std :: shared_ptr?


148

Я переглядав вихідний код Clang і знайшов цей фрагмент:

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

Чому я хочу ?std::movestd::shared_ptr

Чи є якась точка передачі права власності на спільний ресурс?

Чому б я просто не зробив це замість цього?

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

Відповіді:


137

Я думаю, що одне, на що інші відповіді недостатньо підкреслили, це швидкість .

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

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

Зауважте, що ця методика використовується виключно для оптимізації. його копіювання (як ви запропонували) є настільки ж чудовим функціоналом.


5
Це справді сто разів швидше? У вас є орієнтири для цього?
xaviersjs

1
@xaviersjs Присвоєння вимагає збільшення атома з подальшим атомним декрементом, коли значення виходить за межі поля. Атомні операції можуть зайняти сотні циклів годин. Так так, це дійсно так набагато повільніше.
Адісак

2
@Adisak - це перше, що я чув, що операція з добуванням і додаванням ( en.wikipedia.org/wiki/Fetch-and-add ) може зайняти сотні циклів більше, ніж базовий приріст. У вас є посилання на це?
xaviersjs

2
@xaviersjs: stackoverflow.com/a/16132551/4238087 З операцією регістра бути кілька циклів, 100 х (100-300) циклів для атомних пристосовує рахунок. Незважаючи на те, що показники наведені з 2013 року, це все ще видається правдою, особливо для багатомодерних систем NUMA.
russianfool

1
Іноді ти думаєш, що у твоєму коді немає жодної нитки ... але тоді приходить якась проклята бібліотека і руйнує її для u. Краще використовувати посилання const та std :: move ... якщо зрозуміло і очевидно, що ви можете .... ніж покладатися на підрахунки посилань на покажчики.
Ерік Аронесті

123

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


1
Чи це не передчасна оптимізація?
ВАТ

11
@YSC ні, якщо той, хто поставив його там, насправді перевірив.
OrangeDog

19
@YSC Передчасна оптимізація є злом, якщо вона ускладнює читання чи підтримку коду. Цей не робить жодного, принаймні ІМО.
Ендже вже не пишається SO

17
Справді. Це не передчасна оптимізація. Натомість це розумний спосіб написати цю функцію.
Гонки легкості на орбіті

60

Переміщення операцій (наприклад , переміщення конструктора) для std::shared_ptrє дешевим , так як вони в основному є «викрадення покажчики» (від джерела до пункту призначення, дозволяючи бути більш точним, то весь блок державного управління «вкрадено» від джерела до місця призначення, в тому числі інформації підрахунку посилань) .

Натомість операції копіювання на std::shared_ptrвиклику атомного опорного числа збільшуються (тобто не лише ++RefCountна цілий RefCountчлен даних, але, наприклад, виклик InterlockedIncrementв Windows), що дорожче, ніж просто крадіжка покажчиків / стану.

Отже, детально аналізуючи динаміку підрахунку посилань:

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

Якщо ви переходите spза значенням, а потім робите копію всередині CompilerInstance::setInvocationметоду, у вас є:

  1. При введенні методу shared_ptrпараметр будується копією: збільшення числа атомних приростів .
  2. Усередині тіла методу, ви скопіювати в shared_ptrпараметр в елементі даних: ЗАВДАННЯ розраховувати атомне приріст .
  3. При виході з методу shared_ptrруйнується параметр: атомний декремент .

У вас є два атомних прирости і один атомний декремент, в цілому три атомні операції.

Натомість, якщо ви передаєте shared_ptrпараметр за значенням, а потім std::moveвсередині методу (як це правильно зроблено в коді Кланг), у вас є:

  1. При введенні методу shared_ptrпараметр будується копією: збільшення числа атомних приростів .
  2. Усередині тіла методу, ви параметр в елемент даних: вих лічильник нічого НЕ зміниться! Ви просто крадете вказівники / стан: жодних дорогих операцій підрахунку атомних змін не задіяно.std::moveshared_ptr
  3. При виході з методу shared_ptrпараметр руйнується; але оскільки ви перейшли на крок 2, нічого руйнувати не можна, оскільки shared_ptrпараметр більше не вказує ні на що. Знову-таки, ніякого атомного декременту в цьому випадку не відбувається.

Підсумок: у цьому випадку ви отримуєте лише один приріст атомного приросту, тобто лише одну атомну операцію.
Як бачите, це набагато краще, ніж два атомних кроки плюс один атомний декремент (для загальних трьох атомних операцій) для випадку копіювання.


1
Також варто зауважити: чому вони просто не проходять повз посилання const і не уникають цілих матеріалів std :: move? Оскільки пропускна вартість також дозволяє вам безпосередньо переходити в необроблений покажчик, і буде створено один спільний_птр.
Джозеф Ірландія

@JosephI Ireland Тому що ти не можеш перемістити посилання на const
Бруно Феррейра,

2
@ ДжозефІрландія, тому що, якщо ви будете називати його так, compilerInstance.setInvocation(std::move(sp));не буде збільшення . Ви можете отримати таку саму поведінку, додавши перевантаження, яке сприймає, shared_ptr<>&&але чому дублює, коли цього не потрібно.
щурячий вирод

2
@BrunoFerreira Я відповідав на власне запитання. Вам не потрібно буде переміщувати його, оскільки це посилання, просто скопіюйте його. Ще лише одна копія замість двох. Тому вони не роблять цього, тому що це було б зайве скопіювати новозбудовані shared_ptrs, наприклад , від setInvocation(new CompilerInvocation), або як згадано тріскачки, setInvocation(std::move(sp)). Вибачте, якщо мій перший коментар був незрозумілим, я його фактично опублікував випадково, перш ніж я закінчив писати, і я вирішив просто залишити його
Джозеф Ірландія

22

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


16

Існує дві причини використання std :: move в цій ситуації. Більшість відповідей вирішували питання швидкості, але ігнорували важливе питання щодо більш чіткого відображення наміру коду.

Для std :: shared_ptr, std :: move однозначно позначає передачу права власності на pointee, тоді як проста операція копіювання додає додаткового власника. Звичайно, якщо згодом початковий власник відмовляється від власності (наприклад, дозволяючи знищити їх std :: shared_ptr), то передача права власності була здійснена.

Коли ви передаєте право власності на std :: move, це очевидно, що відбувається. Якщо ви використовуєте звичайну копію, не очевидно, що призначена операція є передачею, поки ви не переконаєтесь, що початковий власник негайно відмовиться від права власності. Як бонус можлива більш ефективна реалізація, оскільки атомна передача права власності дозволяє уникнути тимчасового стану, коли кількість власників збільшилася на одиницю (а супутні зміни в еталонних підрахунках).


Саме те, що я шукаю. Дивно, як інші відповіді ігнорують цю важливу смислову різницю. розумні покажчики стосуються власності.
qweruiop

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.