Я читал, что создание или копирование std :: shared_ptr влечет за собой некоторые накладные расходы (атомарный прирост счетчика ссылок и т. Д.).
Но как насчет создания из него std :: weak_ptr:
Obj * obj = new Obj();
// fast
Obj * o = obj;
// slow
std::shared_ptr<Obj> a(o);
// slow
std::shared_ptr<Obj> b(a);
// slow ?
std::weak_ptr<Obj> c(b);
Я надеялся на более высокую производительность, но я знаю, что общий указатель все еще должен увеличивать счетчик слабых ссылок.
Так это все еще так же медленно, как копирование shared_ptr в другой?
В дополнение к Алека Очень интересное описание системы shared / weak_ptr, используемой в его предыдущих проектах, я хотел бы дать немного больше подробностей о том, что, вероятно, происходит в типичном случае. std::shared_ptr/weak_ptr
реализация:
// slow
std::shared_ptr<Obj> a(o);
Основным расходом в вышеупомянутой конструкции является выделение блока памяти для хранения двух ссылок. Здесь не нужно делать никаких атомарных операций (кроме того, что реализация может или не может делать в operator new
).
// slow
std::shared_ptr<Obj> b(a);
Основным расходом в конструкции копии обычно является одно атомное приращение.
// slow ?
std::weak_ptr<Obj> c(b);
Основной расход в этом weak_ptr
Конструктор обычно представляет собой единичный атомарный прирост. Я ожидаю, что производительность этого конструктора будет почти такой же, как у shared_ptr
Копировать конструктор.
Два других важных конструктора, о которых следует знать:
std::shared_ptr<Obj> d(std::move(a)); // shared_ptr(shared_ptr&&);
std::weak_ptr<Obj> e(std::move( c )); // weak_ptr(weak_ptr&&);
(И соответствующие операторы назначения перемещения)
Конструкторы перемещения вообще не требуют атомарных операций. Они просто копируют счетчик ссылок из правого в левое и создают правое число == nullptr.
Операторы назначения перемещения требуют атомарного декремента, только если lhs! = Nullptr до назначения. Большая часть времени (например, в течение vector<shared_ptr<T>>
) lhs == nullptr до назначения перемещения, и поэтому нет никаких атомарных операций вообще.
Последний ( weak_ptr
элементы move) на самом деле не являются C ++ 11, но обрабатываются LWG 2315. Однако я ожидаю, что это уже будет реализовано большинством реализаций (я знаю, что это уже реализовано в Libc ++).
Эти элементы перемещения будут использоваться при перемещении интеллектуальных указателей в контейнерах, например под vector<shared_ptr<T>>::insert/erase
и может оказать ощутимое положительное влияние по сравнению с использованием элементов копирования с интеллектуальным указателем.
Я подчеркиваю это, чтобы вы знали, что если у вас есть возможность переместить вместо того, чтобы скопировать shared_ptr/weak_ptr
, стоит потратить несколько лишних символов, чтобы сделать это.
Это из моих дней с игровыми движками
История идет:
Нам нужна быстрая реализация совместно используемых указателей, которая не будет перегружать кеш (кстати, кеши стали умнее)
Обычный указатель:
XXXXXXXXXXXX....
^--pointer to data
Наш общий указатель:
iiiiXXXXXXXXXXXXXXXXX...
^ ^---pointer stored in shared pointer
|
+---the start of the allocation, the allocation is sizeof(unsigned int)+sizeof(T)
unsigned int*
используется для подсчета в ((unsigned int*)ptr)-1
таким образом, «общий указатель» имеет размер указателя, а содержащиеся в нем данные являются указателем на фактические данные. Так (потому что шаблон=>
inline, и любой компилятор вставлял бы оператор, возвращающий элемент данных), это были те же «издержки» для доступа, что и для обычного указателя.
Создание указателей заняло, как правило, на 3 инструкции ЦП больше, чем обычно (доступ к расположению-4 находится в работе, добавление 1 и запись в местоположение -4)
Теперь мы будем использовать слабые указатели только во время отладки (поэтому мы будем компилировать с определением DEBUG (определение макроса)), потому что тогда мы хотели бы видеть все распределения и что происходит и тому подобное. Это было полезно.
Слабые указатели должны знать, когда исчезает то, на что они указывают, а НЕ сохранять то, на что они указывают, (в моем случае, если слабый указатель поддерживает выделение ресурсов, движок никогда не сможет перезапустить или освободить какую-либо память, тогда это в основном общий указатель в любом случае)
Таким образом, каждый слабый указатель имеет Bool, alive
или что-то, и друг shared_pointer
При отладке наше распределение выглядело так:
vvvvvvvviiiiXXXXXXXXXXXXX.....
^ ^ ^ the pointer we stored (to the data)
| +that pointer -4 bytes = ref counter
+Initial allocation now
sizeof(linked_list<weak_pointer<T>*>)+sizeof(unsigned int)+sizeof(T)
Используемая вами структура связанного списка зависит от того, что вас волнует, мы хотели оставаться как можно ближе к sizeof (T) (мы управляли памятью с помощью алгоритма собеседника), поэтому мы сохранили указатель на weak_pointer и использовали трюк xor. … хорошие времена.
В любом случае: слабые указатели на что-то, на что указывают shared_pointers, помещаются в список, как-то сохраненный в «v» выше.
Когда счетчик ссылок достигает нуля, вы проходите этот список (который является списком указателей на фактические указатели weak_pointers, они удаляются сами, когда явно удалены), и вы устанавливаете живым = ложь (или что-то) для каждого указателя слабого_показателя.
Слабые_поинтеры теперь знают, на что они указывают, там больше нет (поэтому выкинули при разыменовании)
В этом примере
Нет никаких накладных расходов (выравнивание составило 4 байта с системой. 64-битные системы, как правило, любят выравнивание по 8 байтов …. объедините счетчик ref с int [2], чтобы заполнить его в этом случае. Запомните это включает в себя новости на месте (никто не отрицает, потому что я упомянул их: P) и тому подобное. Вы должны убедиться, что struct
Вы накладываете на распределение соответствия то, что вы выделили и сделали. Компиляторы могут выравнивать вещи для себя (следовательно, int [2], а не int, int).
Вы можете отменить ссылку на shared_pointer без дополнительных затрат.
Новые общие указатели, которые делаются, вообще не перебивают кэш и требуют 3-х инструкций ЦП, они не очень … способны к конвейерной линии, но компилятор всегда вставляет получатели и установщики (если не всегда: P) и там ‘ Вокруг колл-сайта будет что-то, что может заполнить конвейер.
Деструктор разделяемого указателя тоже делает очень мало (вот и все), это здорово!
Замечание высокой производительности
Если у вас есть такая ситуация:
f() {
shared_pointer<T> ptr;
g(ptr);
}
Нет гарантии, что оптимизатор не осмелится не делать сложения и вычитания из передачи shared_pointer «по значению» в g.
Здесь вы бы использовали обычную ссылку (которая реализована в виде указателя)
так что вы бы сделали g(ptr.extract_reference());
вместо этого — снова компилятор включит простой метод получения.
теперь у вас есть T&, поскольку область действия ptr полностью окружает g (при условии, что g не имеет побочных эффектов и т. д.), эта ссылка будет действительна в течение g.
удаление ссылок очень уродливо, и вы, вероятно, не могли сделать это случайно (мы полагались на этот факт).
Задним числом
Я должен был создать тип с именем «extract_pointer» или что-то еще, было бы очень сложно напечатать это по ошибке для члена класса.
Слабые / общие указатели, используемые stdlib ++
http://gcc.gnu.org/onlinedocs/libstdc++/manual/shared_ptr.html
Не так быстро …
Но не беспокойтесь о странной потере кэша, если только вы не создаете игровой движок, который не запускает приличную рабочую нагрузку> 120 кадров в секунду: P По-прежнему намного лучше, чем Java.
Путь stdlib лучше. Каждый объект имеет свое распределение и работу. С нашим shared_pointer
это был настоящий случай «поверь мне, это работает, постарайся не беспокоиться о том, как» (не то, чтобы это было сложно), потому что код выглядел действительно грязно
Если вы отмените … что бы они ни делали с именами переменных в их реализации, было бы намного легче читать. Посмотрите реализацию Boost, как сказано в этих документах.
Помимо имен переменных, реализация GCC stdlib прекрасна. Вы можете легко прочитать его, он выполняет свою работу должным образом (следуя принципу ОО), но немного медленнее и МОЖЕТ порвать кэш на дрянных чипах в наши дни.
UBER высокоэффективное примечание
Вы можете думать, почему бы не иметь XXXX...XXXXiiii
(количество ссылок в конце), тогда вы получите выравнивание, которое лучше всего подходит для распределителя!
Ответ:
Потому что надо делать pointer+sizeof(T)
не может быть одной инструкцией процессора! (Вычитание 4 или 8 — это то, что процессор может легко сделать, просто потому что это имеет смысл, он будет делать это много)