Инвазивные и неинвазивные указатели с рефлексией в С++

В течение последних нескольких лет я обычно принимал, что

если я собираюсь использовать интеллектуальные указатели 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. Однако копирование и уничтожение общих указателей может быть немного медленнее. Сказав это, позвольте мне добавить, что я редко использую такие умные указатели. Большую часть времени уникальное совпадение - это все, что мне нужно.