Действительно ли make_shared эффективнее нового?
Я экспериментировал с shared_ptr
и make_shared
с С++ 11 и запрограммировал небольшой пример игрушек, чтобы увидеть, что на самом деле происходит при вызове make_shared
. В качестве инфраструктуры я использовал llvm/clang 3.0 вместе с библиотекой llvm std С++ в XCode4.
class Object
{
public:
Object(const string& str)
{
cout << "Constructor " << str << endl;
}
Object()
{
cout << "Default constructor" << endl;
}
~Object()
{
cout << "Destructor" << endl;
}
Object(const Object& rhs)
{
cout << "Copy constructor..." << endl;
}
};
void make_shared_example()
{
cout << "Create smart_ptr using make_shared..." << endl;
auto ptr_res1 = make_shared<Object>("make_shared");
cout << "Create smart_ptr using make_shared: done." << endl;
cout << "Create smart_ptr using new..." << endl;
shared_ptr<Object> ptr_res2(new Object("new"));
cout << "Create smart_ptr using new: done." << endl;
}
Теперь посмотрите на результат, пожалуйста:
Создайте smart_ptr, используя make_shared...
Конструктор make_shared
Копировать конструктор...
Копировать конструктор...
деструктор
деструктор
Создайте smart_ptr, используя make_shared: done.
Создать smart_ptr с помощью нового...
Конструктор new
Создайте smart_ptr с помощью new: done.
деструктор
деструктор
Похоже, что make_shared
вызывает конструктор копирования два раза. Если я выделяю память для Object
с помощью регулярного new
, это не произойдет, будет создан только один Object
.
Что мне интересно, так это следующее. Я слышал, что make_shared
должен быть более эффективным, чем использование new
(1, 2). Одна из причин заключается в том, что make_shared
выделяет счетчик ссылок вместе с объектом, который должен управляться в том же блоке памяти. Хорошо, я понял. Это, конечно, более эффективно, чем две отдельные операции выделения.
Напротив, я не понимаю, почему это связано с стоимостью двух вызовов конструктора копирования Object
. Из-за этого я не уверен, что make_shared
более эффективен, чем распределение, используя new
в каждом случае. Я здесь не прав? Хорошо, можно реализовать конструктор перемещения для Object
, но все же я не уверен, что это более эффективно, чем просто выделить Object
через new
. По крайней мере, не в каждом случае. Было бы правдой, если копирование Object
было бы дешевле, чем выделение памяти для контрольного счетчика. Но внутренний счетчик shared_ptr
может быть реализован с использованием пары примитивных типов данных, правда?
Можете ли вы помочь и объяснить, почему make_shared
- это способ работать с точки зрения эффективности, несмотря на наложенные на них накладные расходы?
Ответы
Ответ 1
В качестве инфраструктуры я использовал llvm/clang 3.0 вместе с библиотекой llvm std С++ в XCode4.
Хорошо, что это твоя проблема. Стандарт С++ 11 устанавливает следующие требования для make_shared<T>
(и allocate_shared<T>
) в разделе 20.7.2.2.6:
Требуется: выражение:: new (pv) T (std:: forward (args)...), где pv имеет тип void * и указывает на хранилище, подходящее для хранения объекта типа T, должно быть хорошо сформировано, A - распределитель (17.6.3.5). Конструктор копирования и деструктор А не должны исключать исключения.
T
не требуется для копирования. Действительно, T
даже не требуется, чтобы он не был помещен в новый конструктивный. Это необходимо только для создания на месте. Это означает, что единственное, что make_shared<T>
может делать с T
, это new
на месте.
Таким образом, полученные вами результаты не соответствуют стандарту. LLVM libС++ нарушен в этом отношении. Введите отчет об ошибке.
Для справки, вот что произошло, когда я взял ваш код в VC2010:
Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor
Destructor
Я также портировал его на Boost original shared_ptr
и make_shared
, и я получил то же, что и VC2010.
Я предлагаю подать отчет об ошибке, поскольку поведение libС++ нарушено.
Ответ 2
Вам нужно сравнить эти две версии:
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));
В вашем коде вторая переменная - всего лишь голой указатель, а не общий указатель.
Теперь о мясе. make_shared
(на практике) более эффективен, поскольку он выделяет контрольный блок управления вместе с фактическим объектом в одном динамическом распределении. Напротив, конструктор для shared_ptr
, который принимает открытый объектный указатель, должен выделять другую динамическую переменную для счетчика ссылок. Компромисс заключается в том, что make_shared
(или его двоюродный брат allocate_shared
) не позволяет указать пользовательский делектор, так как распределение выполняется распределителем.
(Это не влияет на конструкцию самого объекта.С точки зрения Object
нет никакой разницы между двумя версиями. Что более эффективно - это сам общий указатель, а не управляемый объект.)
Ответ 3
Итак, нужно иметь в виду ваши настройки оптимизации. Измерение производительности, особенно в отношении С++, бессмысленно без оптимизации. Я не знаю, действительно ли вы делали компиляцию с оптимизацией, поэтому я подумал, что стоит упомянуть.
Тем не менее, то, что вы измеряете с помощью этого теста, не, способ make_shared
более эффективен. Проще говоря, вы измеряете неправильную вещь: -P.
Здесь сделка. Обычно, когда вы создаете общий указатель, у него есть как минимум 2 члена данных (возможно, больше). Один для указателя и один для подсчета ссылок. Этот счетчик ссылок выделяется в куче (чтобы он мог делиться среди shared_ptr
с разными сроками жизни... что точка в конце концов!)
Итак, если вы создаете объект с чем-то вроде std::shared_ptr<Object> p2(new Object("foo"));
По крайней мере 2 вызовы new
. Один для Object
и один для объекта подсчета ссылок.
make_shared
имеет параметр (я не уверен, что он должен), чтобы сделать один new
, который достаточно велик, чтобы удерживать объект, на который указывает, и счетчик ссылок в том же непрерывном блоке. Эффективное распределение объекта, который выглядит примерно так (иллюстративно, а не буквально, что это такое).
struct T {
int reference_count;
Object object;
};
Поскольку счетчик ссылок и время жизни объекта связаны друг с другом (для одного человека не имеет смысла жить дольше другого). Весь этот блок может быть delete
d в то же время.
Таким образом, эффективность заключается в распределении, а не в копировании (что, как я подозревал, было связано с оптимизацией больше всего на свете).
Чтобы быть ясным, это то, что нужно сказать о make_shared
http://www.boost.org/doc/libs/1_43_0/libs/smart_ptr/make_shared.html
Помимо удобства и стиля, такая функция также безопасна для исключений и значительно быстрее, поскольку он может использовать одно распределение для как объект, так и соответствующий ему блок управления, устраняя значительная часть накладных расходов shared_ptr. Эта устраняет одну из основных жалоб на эффективность для shared_ptr.
Ответ 4
У вас не должно быть никаких дополнительных копий. Выход должен быть:
Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor
Я не знаю, почему вы получаете дополнительные копии. (хотя я вижу, что у вас слишком много "Destructor", поэтому код, который вы использовали для получения вашего вывода, должен отличаться от кода, который вы опубликовали)
make_shared
более эффективен, потому что он может быть реализован с использованием только одного динамического распределения вместо двух, и потому, что ему требуется один указатель на сумму памяти, меньшую, чем при хранении, на один общий объект.
Изменить: я не проверял с Xcode 4.2, но с Xcode 4.3 я получаю правильный вывод, который я показываю выше, а не неправильный вывод, указанный в вопросе.