Зачем мне std :: переместить std :: shared_ptr?

148

Я просматривал исходный код Clang и нашел этот фрагмент:

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

Зачем мне std::moveэто std::shared_ptr?

Есть ли смысл передавать право собственности на общий ресурс?

Почему бы мне просто не сделать это вместо этого?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}
sdgfsdh
источник

Ответы:

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
Иногда вы думаете, что в вашем коде нет многопоточности ... но затем появляется какая-то проклятая библиотека и разрушает ее для вас. Лучше использовать константные ссылки и std :: move ..., если ясно и очевидно, что вы можете ...., чем полагаться на счетчик ссылок на указатели.
Эрик Аронесты
123

При использовании moveвы избегаете увеличения, а затем сразу же уменьшения количества акций. Это может сэкономить вам некоторые дорогостоящие атомарные операции по счету использования.

Бо Перссон
источник
1
Разве это не преждевременная оптимизация?
СМУ
11
@ YSC нет, если тот, кто положил его туда, действительно проверял это.
OrangeDog
19
@YSC Преждевременная оптимизация - это зло, если она затрудняет чтение или сопровождение кода. Этот не делает ни один, по крайней мере, ИМО.
Angew больше не гордится 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параметр создается с помощью копии: ref count атомарный инкремент .
  2. Внутри тела метода, вы скопировать в shared_ptrпараметр в элементе данных: ЗАДАНИЕ рассчитывать атомное приращение .
  3. При выходе из метода shared_ptrпараметр разрушается: ref count атомарный декремент .

У вас есть два атомных приращения и один атомарный декремент, всего три атомарных операции.

Вместо этого, если вы передадите shared_ptrпараметр по значению, а затем std::moveвнутри метода (как это правильно сделано в коде Кланга), вы получите:

  1. При входе в метод shared_ptrпараметр создается с помощью копии: ref count атомарный инкремент .
  2. Внутри тела метода, вы параметр в элемент данных: исх счетчик ничего не изменится! Вы просто крадете указатели / состояния: дорогостоящие операции по подсчету атомных ссылок не требуются.std::moveshared_ptr
  3. При выходе из метода shared_ptrпараметр уничтожается; но поскольку вы перешли на шаге 2, уничтожать нечего, так как shared_ptrпараметр больше ни на что не указывает. Опять же, в этом случае не происходит атомного декремента.

Итог: в этом случае вы получаете только один атомный инкремент подсчета ссылок, т.е. только одну атомарную операцию.
Как вы можете видеть, это намного лучше, чем два атомарных приращения плюс один атомарный декремент (всего три атомарных операции) для случая копирования.

Mr.C64
источник
1
Также стоит отметить: почему они просто не проходят по константной ссылке и избегают всего, что связано с std :: move? Потому что передача по значению также позволяет напрямую передавать необработанный указатель, и будет создан только один shared_ptr.
Джозеф Ирландия
@JosephI Ireland Потому что вы не можете перемещать константную ссылку
Бруно Феррейра
2
@JosephI Ирландия, потому что если вы называете это как compilerInstance.setInvocation(std::move(sp));то не будет никакого прироста . Вы можете получить то же поведение, добавив перегрузку, которая требует, shared_ptr<>&&но зачем дублировать, когда вам не нужно.
фрик с трещоткой
2
@ BrunoFerreira Я отвечал на свой вопрос. Вам не нужно перемещать его, потому что это ссылка, просто скопируйте его. Еще только одна копия вместо двух. Причина, по которой они этого не делают, заключается в том, что он будет излишне копировать вновь созданные shared_ptrs, например, из setInvocation(new CompilerInvocation)или как упоминалось с храповиком setInvocation(std::move(sp)). Извините, если мой первый комментарий был неясным, я действительно опубликовал его случайно, до того как закончил писать, и решил просто оставить его
Джозеф Ирландия
22

Копирование shared_ptrвключает в себя копирование его внутреннего указателя объекта состояния и изменение счетчика ссылок. Его перемещение требует только замены указателей на внутренний счетчик ссылок и принадлежащий объект, так что это быстрее.

SingerOfTheFall
источник
16

В этой ситуации есть две причины использования std :: move. Большинство ответов касались проблемы скорости, но игнорировали важную проблему более четкого представления намерений кода.

Для std :: shared_ptr std :: move однозначно обозначает передачу права владения pointee, тогда как простая операция копирования добавляет дополнительного владельца. Конечно, если первоначальный владелец впоследствии отказывается от своего права собственности (например, разрешив уничтожить его std :: shared_ptr), тогда передача права собственности была завершена.

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

Стивен С. Сталь
источник
Именно то, что я ищу. Удивлен, как другие ответы игнорируют эту важную семантическую разницу. умные указатели - все о собственности.
qweruiop