Инвазивные и неинвазивные указатели с рефлексией в С++
В течение последних нескольких лет я обычно принимал, что
если я собираюсь использовать интеллектуальные указатели ref-counted
инвазивные интеллектуальные указатели - это путь
-
Однако я начинаю любить неинвазивные умные указатели из-за следующего:
- Я использую только интеллектуальные указатели (так что Foo * не лежит вокруг, а только Ptr)
- Я начинаю создавать пользовательские распределители для каждого класса. (Таким образом, Foo перегрузит новый оператор).
- Теперь, если Foo имеет список всех Ptr (как легко это сделать с неинвазивными интеллектуальными указателями).
- Тогда я могу избежать проблем с фрагментацией памяти, так как класс Foo перемещает объекты вокруг (и просто обновляет соответствующий Ptr).
Единственная причина, по которой эти движущиеся объекты Foo в неинвазивных интеллектуальных указателях проще, чем инвазивные интеллектуальные указатели:
В неинвазивных интеллектуальных указателях есть только один указатель, указывающий на каждый Foo.
В инвазивных интеллектуальных указателях я понятия не имею, сколько объектов указывает на каждый Foo.
Теперь единственная стоимость неинвазивных интеллектуальных указателей... - это двойная косвенность. [Возможно, это закручивает кеши).
Есть ли у кого-нибудь хорошее изучение дорогого этого дополнительного слоя косвенности?
EDIT: с помощью интеллектуальных указателей я могу ссылаться на то, что другие называют "shared-pointers"; вся идея такова: есть счетчик ссылок, прикрепленный к объектам, и когда он достигает 0, объект автоматически удаляется
Ответы
Ответ 1
Существует несколько важных различий между инвазивными или неинвазивными указателями:
Самое большое преимущество второго (неинвазивного):
- Намного проще реализовать слабую ссылку на вторую (т.е.
shared_ptr
/weak_ptr
).
Преимущество первого заключается в том, когда вам нужно получить умный указатель на это (по крайней мере, в случае boost::shared_ptr
, std::tr1::shared_ptr
)
- Вы не можете использовать
shared_ptr
из этого в конструкторе и деструкторе.
- Совершенно нетривиально, чтобы это разделилось в иерархии классов.
Ответ 2
Хорошо, в первую очередь, я напомню вам, что совместное владение обычно является трудным зверем для приручения и может привести к довольно сложному исправить ошибки.
Существует много способов не иметь совместного владения. Подход Factory
(реализованный с помощью Boost Pointer Container) является лично одним из моих любимых.
Теперь, что касается подсчета ссылок,...
1. Интрузивные указатели
Счетчик встроен в сам объект, что означает:
- вам необходимо предоставить методы для добавления/вычитания на счетчик, и ваша обязанность сделать их потокобезопасными.
- счетчик не выдерживает объект, поэтому не
weak_ptr
, поэтому вы не можете иметь циклы ссылок в своем дизайне без использования шаблона Observer
... довольно сложно
2. Неинтрузивные указатели
Я буду говорить только о boost::shared_ptr
и boost::weak_ptr
. Я недавно врылся в источник, чтобы точно посмотреть на механику, и, действительно, намного сложнее, чем выше!
// extract of <boost/shared_ptr.hpp>
template <class T>
class shared_ptr
{
T * px; // contained pointer
boost::detail::shared_count pn; // reference counter
};
- Обслуживание счетчика уже выписано для вас и является потокобезопасным.
- Вы можете использовать
weak_ptr
в случае циклических ссылок.
- Только одно здание, объект
shared_ptr
должен знать о деструкторе объекта (см. пример)
Вот небольшой пример, чтобы проиллюстрировать эту волшебную манеру декларации:
// foofwd.h
#include <boost/shared_ptr.hpp>
class Foo;
typedef boost::shared_ptr<Foo> foo_ptr;
foo_ptr make_foo();
// foo.h
#include "foofwd.h"
class Foo { /** **/ };
// foo.cpp
#include "foo.h"
foo_ptr make_foo() { return foo_ptr(new Foo()); }
// main.cpp
#include "foofwd.h"
int main(int argc, char* argv[])
{
foo_ptr p = make_foo();
} // p.get() is properly released
Существует несколько шаблонов, чтобы разрешить это. В принципе, объект-счетчик вставляет disposer*
(еще третье выделение), что позволяет выполнить некоторое стирание типа. Действительно полезно, хотя, поскольку он действительно разрешает прямое объявление!
3. Заключение
Хотя я согласен с тем, что Intrusive Pointers, вероятно, быстрее, чем при меньшем распределении (имеется 3 разных блока памяти, выделенных для shared_ptr
), также менее практичны.
Итак, я хотел бы указать вам на библиотеку Boost Intrusive Pointer и, более конкретно, на ее введение:
Как правило, если неясно, подходит ли intrusive_ptr
для ваших нужд, чем shared_ptr
, сначала попробуйте дизайн на основе shared_ptr
.
Ответ 3
Я не знаю исследования о дополнительных расходах из-за неинвазивного над инвазивностью. Но я хотел бы отметить, что неинвазивные, как представляется, универсально рекомендуются экспертами С++. Конечно, это ничего не значит! Но аргументация довольно проста: если вам нужны интеллектуальные указатели, это потому, что вам нужен более простой способ реализовать управление жизненным циклом объекта, вместо того чтобы писать его вручную, поэтому вы подчеркиваете правильность и простоту в отношении производительности, что всегда является хорошей идеей, пока вы не профилировали реалистичную модель всего вашего дизайна.
Вполне возможно, что в упрощенном тесте неинвазивное в два раза медленнее, чем инвазивное, и все же в реальной программе, которая действительно работает, эта разница в скорости теряется в шуме и становится настолько незначительной, что вы не можете даже измерить его. Это довольно распространенное явление; вещи, которые вы себе представляете, важны для производительности очень часто.
Если вы обнаружите узкое место в производительности, возможно ли (вероятно?), что работа по поддержанию самого ссылочного счета (в обоих подходах) будет иметь такое же влияние на производительность, как и дополнительная косвенность в неинвазивном подходе. С необработанными указателями утверждение:
p1 = p2;
возможно, только нужно переместить значение между двумя регистрами ЦП, после того как оптимизатор проработал свою магию. Но если они ссылаются на подсчеты интеллектуальных указателей, даже с инвазивным они выглядят так:
if (p1 != p2)
{
if ((p1 != 0) && (--(p1->count) == 0))
delete p1;
p1 = p2;
if (p1 != 0)
p1->count++;
}
Это происходит с каждым аргументом умного указателя, переданным каждой функции. Таким образом, есть много дополнительных доступов к потенциально отдаленным областям памяти, чтобы каждый раз поднимать и опускать счет. Чтобы быть потокобезопасными, операции инкремента и декремента должны быть взаимно блокированы/атомарны, что может оказать серьезное отрицательное влияние на несколько ядер.
Я думаю о "сладком месте" С++ как о тех ситуациях, когда вам не нужно управлять динамически динамическими структурами данных, как это. Вместо этого у вас есть простой иерархический шаблон владения объектами, поэтому есть очевидный одиночный владелец каждого объекта, а время жизни данных имеет тенденцию следовать за временем жизни вызовов функций (чаще всего). Затем вы можете позволить стандартным контейнерам и стеку вызовов функций управлять всем для вас. Это подчеркивается в предстоящей версии языка с rvalue-ссылками, unique_ptr
и т.д., Что связано с тем, что он легко переносится вокруг единого владения объектом. Если вам действительно нужно управление динамическим многопользовательским ресурсом, истинный GC будет быстрее и удобнее в использовании, но С++ не очень счастлив для GC.
Еще один второстепенный момент: к сожалению, "в неинвазивных интеллектуальных указателях есть только один указатель, указывающий на каждый Foo", не соответствует действительности. Внутри Foo
есть указатель this
, который является Foo *
, и поэтому голой указатель все же может быть просочился, часто с довольно сложными способами.
Ответ 4
Единственная реальная стоимость неинвазивного счета ref w.r.t. производительность - это то, что вам иногда требуется дополнительное выделение для счетчика ref. Насколько я знаю, реализации tr1:: shared_ptr не выполняют "двойную косвенность". Я полагаю, было бы сложно поддерживать конверсии, не позволяя shared_ptr хранить указатель напрямую. Разумная реализация shared_ptr сохранит два указателя: один указатель на объект (без двойной косвенности) и один указатель на некоторую структуру управления.
Даже накладные расходы на распределение необязательны во всех ситуациях. См. make_shared. С++ 0x также предоставит функцию make_shared
, которая выделяет как объект, так и рефректор в один проход, который аналогичен по отношению к интрузивной альтернативе ref-counting.
[...] Помимо удобства и стиля, такая функция также исключает безопасность и значительно быстрее, поскольку она может использовать единое распределение как для объекта, так и для соответствующего блока управления, исключая значительную часть затрат на строительство shared_ptr. Это устраняет одну из основных жалоб на эффективность использования shared_ptr. [...]
В свете shared_ptr
и make_shared
я с трудом сталкиваюсь с проблемами, когда интенсивные умные указатели будут сильно бить shared_ptr
. Однако копирование и уничтожение общих указателей может быть немного медленнее. Сказав это, позвольте мне добавить, что я редко использую такие умные указатели. Большую часть времени уникальное совпадение - это все, что мне нужно.