C ++ слабое_производительность создания

Я читал, что создание или копирование 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 в другой?

4

Решение

В дополнение к Алека Очень интересное описание системы 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, стоит потратить несколько лишних символов, чтобы сделать это.

10

Другие решения

Это из моих дней с игровыми движками

История идет:

Нам нужна быстрая реализация совместно используемых указателей, которая не будет перегружать кеш (кстати, кеши стали умнее)

Обычный указатель:

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 — это то, что процессор может легко сделать, просто потому что это имеет смысл, он будет делать это много)

14

По вопросам рекламы [email protected]