Подтвердить что ты не робот

Действительно ли 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 - это способ работать с точки зрения эффективности, несмотря на наложенные на них накладные расходы?

4b9b3361

Ответ 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 я получаю правильный вывод, который я показываю выше, а не неправильный вывод, указанный в вопросе.